Переклад українською - Арсеній
Чеботарьов - Ніжин 2016
Що
таке Akka?
Маштабуєма обробка
транзакцій в реальному часі
Ми
вважаємо, що написання коректного, конкурентного, стійкого до відмов та
маштабуємого застосування є дуже складним. Більшість часу це завдяки тому,
що ми використовуємо хибні інструменти та хибний рівень абстрайкцій. Akka
створений щоб змінити це. Використовуючи Модель Акторів ми підіймаємо
рівень абстракції, та провадимо кращу платформу для побудови маштабуємих,
стійких та реактивних застосувань — дивіться маніфест Reactive
Manifesto щоб отримати додаткові
деталі. Для стійкості до відмов ми адаптували модель "нехай
ламається", що використовується в індустрії телекомунікацій з великим
успіхом для побудови само-виліковних застосувань, та систем, що ніколи не
зупиняються. Актори також провадять абстракцію для прозорого
розповсюдження та базис для дійсно маштабованих та стійких до відмов
застосувань.
Akka
є Open Source та доступна під Apache 2 License.
Завантайжуйте
з http://akka.io/downloads.
Будь
ласка зауважте, що всі приклади коду компілюються, так що якщо ви бажаєте
мати прямий доступ до первинного коду, погляньте на підпроект Akka Docs на
github: для Java
та Scala.
Akka
реалізує унікальний гібрид
Актори
Актори
надають вам:
- Прості абстракції високого рівня для
конкурентності та паралелізму.
- Асинхронну, неблокуючу та
високопродуктивну модель програмування, що базується на
повідомленнях.
- Дуже легковажні, рухомі повідомленнями,
процеси (декілька мільйонів акторів на на Гб пам'яті купи).
Дивіться
главу для Scala або
Java.
Стійкість
до відмов
- Ієрархії супервізорів з семантикою
"нехай ламається" ("let-it-crash").
- Ієрархії супервізорів можуть охоплювати
декілька JVM щоб провадити стійки до відмов системи.
- Чудове для написання високостійких до
відмов систем, що виліковують себе та ніколи не зупиняються.
Дивіться Стійкість
до відмов (Scala) and Стійкість
до відмов (Java).
Прозорість
до розташування
Все
в Akka розоблене для роботи в розподіленому середовищі: всі інтеракції
акторів використовують чисту передачу повідомлень, та все є
асинхронним.
Для
огляду підтримки кластерів дивіться глави документації
Java та
Scala.
Постійність
Повідомлення,
отримані актором, опціонально можуть бути постійними, та
відтворюватись, коли актор стартує або рестартує. Це дозволяє акторам
відтворювати власний стан, навіть якщо JVM зазнає краху, або коли
актор мігрує на інший вузол.
Ви
можете знайти більше деталей в відповідній главі для
Java або
Scala.
Akka
може використовуватись в два різні способа
Akka
може використовуватись та розгортатись в різний спосіб:
- Як бібліотека: використовуватись як
звичайний JAR на classpath та/або в веб застосунку, та розташовуватись
в
WEB-INF/lib
- Пакується за допомогою sbt-native-packager
- Пакується та розгортається з
використанням Typesafe
ConductR.
Чому
Akka?
Які
гарні приклади застосування Akka?
Ми
очікуємо, що Akka буде адаптований багатьма великими організаціями в
великому диапазоні індустрій:
- Інвестиції та банківська справа
- Продажі
- Соціальні медіа
- Симуляція
- Ігри та ставки
- Системи автомобільного трафіку
- Охорона здоров'я
- Аналіз даних
та
багато інших. Люба система з потребою до високої пропускної
спроможності та низької латентності є гарним кандидатом для
використання Akka.
Актори
дозволяють вам керувати відмовами сервісів (функції супервізора),
керування навантаженням (стратегії відступу, таймаути та ізоляція
обробки), так само, як горизонтальне та вертикальне маштабування
(додають більше ядер та/або додають більше машин).
Ось
що деякі користувачі Akka кажуть щодо того, як вони
використовують Akka: http://stackoverflow.com/questions/4493001/good-use-case-for-akka
Починаємо
Попередні
вимоги
Akka
потребує, щоб ви мали Java 8 або старшу встановленою на вашою
машині.
Typesafe провадить коменційний білд Akka
та пов'язаних проектів, таких, як Scala або Play, як частину Reactive
Platform, що зроблена
доступною для Java 6 в разі, якщо ваш проект все ще не
може бути перенесений на Java 8. Він також включає додаткові
комерційні можливості або бібліотеки.
Вказівники
для початківців та шаблони проектів
Найкращий
шлях почати вивчати Akka - завантажити Typesafe
Activator та спробувати
один з шаблонів проекту Akka.
Завантаження
Є
декілька шляхів завантажити Akka. Ви можете завантажити його як
частину Typesafe Platform (як зазначено вище). Ви можете завантажити
повну дистрибуцію, що включає всі модулі. Або ви можете використати
інструменти побудови, такі, як Maven або SBT, для завантаження
залежностей з Maven репозитарія Akka.
Модулі
Akka
є
дуже модулярним, та складається з декількох JAR, що містять різні
можливості.
akka-actor – Касичні актори,
типізовані актори, IO актори etc.
akka-agent – Агенти, інтегровані
зі Scala STM
akka-camel – Інтеграція з Apache
Camel
akka-cluster – Керування
кластерами, еластична маршрутизіція
akka-osgi – Утілити для
використання Akka в контейнерах OSGi
akka-osgi-aries – Проект Aries для
разервування системів акторів
akka-remote – Віддалені актори
akka-slf4j – SLF4J Logger
(слухач шини подій)
akka-testkit – Інструменти для
тестування систем акторів
На
додаток до ціх стабільних модулів є ще декілька, які на своєму шляху
в стабільне ядро, але наразі все ще марковані як "експерементальні".
Це не означає, що вони не функціонують як задумані. Це, в основному,
означає, що їх API ще не достатньо сталі, щоб вважатись
замороженими. Ви можете допомогти прискорити цей процес, надаючи
зворотній зв'язок по цім модулям в список розсилки.
akka-contrib – різноманітні
контрибуції, що можуть, або ні, переміститись до головних модулей,
дивіться Експериментальні
контрибуції для
додаткових деталей.
Ім'я
файлу дійсного JAR, наприклад, akka-actor_2.11-2.4.1.jar (та аналогічно для
інших модулей).
Побачити
залежності JAR для кожного модуля Akka можна в розділі Залежності.
Використання
версії снепшоту
Нічні
снепшоти Akka публікуються на http://repo.akka.io/snapshots/ та мають версії з обома, SNAPSHOT
та маркерами часу. Ви можете обрати версію з маркером часу, якщо ви
бажаєте робити з ними, та можете потім вирішити оновитись до новішої
версії.
Попередження
Використання
Akka SNAPSHOT, нічних та віхових релізів не заохочується, за
винятком випадку коли ви знаєте, що робите.
Використання
Akka з Maven
Простіший
шлях, щоб почати з Akka та Maven є звернутись до підручника Typesafe
Activator з назвою Akka Main
in Java.
Оскільки
Akka опублікований на Maven Central (для версій починаючи з 2.1-M2),
достатньо додати залежності Akka до POM. Наприклад, ось залежності
для akka-actor:
- com.typesafe.akka
- akka-actor_2.11
- 2.4.1
Для
версій снепшоту також повинна бути додана залежність до репозирарію
снепшоту:
-
- akka-snapshots
-
- true
-
- http://repo.akka.io/snapshots/
-
Зауваження:
версії снепшотів публікуються як
SNAPSHOT та версії з маркерами
часу.
Використання
Akka з SBT
Простіший
шлях почати з Akka та SBT є завантажити шаблон проекту Akka/SBT.
Підсумок
базових частин для використання Akka з SBT:
Інструкції
інсталяції SBT на https://github.com/harrah/xsbt/wiki/Setup
build.sbt файл:
- name := "My Project"
-
- version := "1.0"
-
- scalaVersion := "2.11.7"
-
- libraryDependencies +=
- "com.typesafe.akka" %% "akka-actor" % "2.4.1"
Зауваження:
налаштування libraryDependencies, надані вище, специфічні до SBT
v0.12.x та вище. Якщо ви використовуєте старішу версію SBT,
libraryDependencies має виглядати наступним чином:
- libraryDependencies +=
- "com.typesafe.akka" % "akka-actor_2.11" % "2.4.1"
Для
спепшот версій потрібно також додати репозитарій снепшоту:
- resolvers += "Akka Snapshot Repository" at "http://repo.akka.io/snapshots/"
Використання
Akka з Gradle
Потребує
щонайменьше Gradle 1.4 з використанням Scala
plugin
- apply plugin: 'scala'
-
- repositories {
- mavenCentral()
- }
-
- dependencies {
- compile 'org.scala-lang:scala-library:2.11.7'
- }
-
- tasks.withType(ScalaCompile) {
- scalaCompileOptions.useAnt = false
- }
-
- dependencies {
- compile group: 'com.typesafe.akka', name: 'akka-actor_2.11', version: '2.4.1'
- compile group: 'org.scala-lang', name: 'scala-library', version: '2.11.7'
- }
Для
версій снепшоту також треба додати репозитарій снепшотів:
- repositories {
- mavenCentral()
- maven {
- url "http://repo.akka.io/snapshots/"
- }
- }
Використання
Akka з Eclipse
Встановіть
проект SBT та потім використовуйте sbteclipse для генерації проекту Eclipse.
Використання
Akka з IntelliJ IDEA
Встановіть
проект SBT та потім використовуйте sbt-idea для генерації проекту
IntelliJ IDEA.
Використання
Akka з NetBeans
Встановіть
проект SBT та потім використовуйте nbsbt для генерації проекту NetBeans.
Ви
можете також використовувати nbscala для загальної підтримки scala
в IDE.
Не
використовуйте флаг Scala компілятора -optimize
Попередження
Akka
не був скомпільований або протестований з флагом Scala компілятора
-optimize. Є повідомлення від користувачів, що спробували його
використання, про дивну поведінку.
Побудова
з джерельних текстів
Akka
використовує Git та розташований на
Github.
Продовжіть
читання на сторінці Building
Akka
Потрібна
допомога?
Якщо
ви маєте запитання, ви можете отримати допомогу в Akka
Mailing List.
Ви
можете також запросити комерційну
підтримку.
Дякуємо,
що ви є частиною спільноти Akka.
Обов'язковий
Hello World
Версія
на основі акторів для складної проблеми друку добре відомого
привітання на консоль представлена в підручнику Typesafe
Activator в розділі Akka
Main in Scala.
Інструкція
ілюструє головний клас запуску akka.Main, що очікує тільки один
аргумент командного рядка: ім'я класу головного актора застосування.
Цей головний метод буде створювати інфраструктуру, потрібну для
виконання акторів, запускає наданий головний актор, та керує цілим
застосуванням, щоб завершитись, коли завершується головний
актор.
Є
також інша інструкція Typesafe
Activator з того ж
розділу знань, що називається Hello
Akka!. Він описує основи Akka більш глибоко.
Застосування
та сценарії розгортання
Як
я можу застосовувати та розгортати Akka?
Akka
може
бути використаний в різний спосіб:
- Як бібліотека: використовуватись
як звичайний JAR на classpath та/або в веб застосунку, та
розташовуватись в
WEB-INF/lib
- Пакується за допомогою sbt-native-packager
- Пакується та розгортається з
використанням Typesafe
ConductR.
Природний
пакувальник
sbt-native-packager є інструментом для створення
дистрибутивів любого типу застосувань, всключи Akka
застосування.
Визначте
версію sbt в файлі project/build.properties:
Додайте sbt-native-packager в файл
project/plugins.sbt:
- addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.0.0-RC1")
Використовуйте
налаштування пакунку та опціонально вкажіть mainClass в
файлі build.sbt:
- import NativePackagerHelper._
-
- name := "akka-sample-main-scala"
-
- version := "2.4.1"
-
- scalaVersion := "2.11.7"
-
- libraryDependencies ++= Seq(
- "com.typesafe.akka" %% "akka-actor" % "2.4.1"
- )
-
- enablePlugins(JavaServerAppPackaging)
-
- mainClass in Compile := Some("sample.hello.Main")
-
- mappings in Universal ++= {
- // опціональний приклад, що ілюструє копіювання додаткового каталогу
- directory("scripts") ++
- // копіювання файлів конфігурації до каталогу конфігурації
- contentOf("src/main/resources").toMap.mapValues("config/" + _)
- }
-
- // додати каталог 'config' до classpath стартового скрипту,
- // альтернативно встановити розміщення файлу конфігурації через параметри CLI
- // при запуску застосування
- scriptClasspath := Seq("../config/") ++ scriptClasspath.value
Зауваження
Використовуйте JavaServerAppPackaging.
Не використовуйте AkkaAppPackaging (що раніше
називався packageArchetype.akka_application,
оскільки він не має тієї ж гнучкості та якості, як JavaServerAppPackaging.
Використовання
завдання sbt dist запакує застосування.
Щоб
розпочати застосування (на unix подібній системі):
- cd target/universal/
- unzip akka-sample-main-scala-2.4.1.zip
- chmod u+x akka-sample-main-scala-2.4.1/bin/akka-sample-main-scala
- akka-sample-main-scala-2.4.1/bin/akka-sample-main-scala sample.hello.Main
Використовуйте Ctrl-C щоб перервати та вийти з
застосування.
На
Windows машині ви можете також використати скриптbin\akka-sample-main-scala.bat.
Приклади
застосувань Akka
Ми
сподіваємось, що Akka буде адаптовано багатьма великими
організаціями в великому диапазоні індустрій, від інвестицій та
банківської справи, продажів та соціальних медіа, ігорного бізнесу
та ставок, систем автомобільного трафіку, охорони здоров'я та
багато іншого. Люба система з потребою до високої пропускної
спроможності та низької латентності є гарним кандидатом для
використання Akka.
Точиться
велика дискуссія щодо застосування Akka з деякими гарними
промовами від промислових користувачів
тут
Ось
деякі галузі, де було розвернуто Akka в промисловому оточенні
Основа
для сервісів (будь яка індустрія, будь яке застосування)
Сервіси REST, SOAP, Cometd, WebSockets etc. Діє як
хаб/рівень інтеграції. Маштабування вгору, маштабування
вширину, стійкість до відмов, висока доступність
Конкурентність/паралелізм
(любе застосування)
Коректно, просто до застосування та розуміння, просте
додавання jar до існуючого проекту (використання Scala,
Java, Groovy, або JRuby)
Симуляція
Мастер/робітник, обчислювальні сітки, MapReduce etc.
Пакетна
обробка (люба індустрія)
Інтеграція з Camel для перехоплення пакетних джерел
даних, актори поділяюють пакетне навантаження
Гральна
індустрія та ставки (MOM, онлайн ігри, ставки)
Маштабування вгору, маштабування вширину, стійкість до
відмов, висока доступність
Бізнес
логіка/добування даних/обробники загального призначення
Маштабування вгору, маштабування вширину, стійкість до
відмов, висока доступність
Обробка
складних потоків подій
Маштабування вгору, маштабування вширину, стійкість до
відмов, висока доступність
Термінологія,
концепції
В
цій главі ми спробуємо встановити загальну термінологію для
визначення солідного підгрунтя комунікації щодо конкурентних,
розподілених систем, на які націлений Akka. Будь ласка, майте на
увазі, що для більшості з ціх термінів немає загально
узгодженного визначення. Ми просто бажаємо знайти робочі
визначення, що будуть використовуватись в межах документації
Akka.
Конкурентність
vs. паралелізм
Конкурентність
та паралелізм є пов'язаними концепціями, але є невеликі
розбіжності. Конкурентність означає, що два або
більше завдань просуваються вперед, навіть якщо вони не можуть
виконуватись одночасно. Це може бути досягнено, наприклад, за
допомогою кватнів часу, коли частини завдань виконуюються
послідовно, та перемішуються з частинами інших завдань. Паралелізм, з іншого боку,
виникає, коли виконання дійсно може відбуватись одночасно.
Асинхронність
vs. синхронність
Виклик
методу вважається синхронним,
якщо викликач не може просуватись, доки метод не поверне
значення, або викличе виключення. З іншого
боку, асинхронний виклик дозволяє
викликачу просуватись далі на обмежену кількість кроків, та
про завершення методу може бути повідомлено через додатковий
механізм (це може бути зареєстрований зворотній виклик,
Future, або повідомлення).
Синхронне
API може використовувати блокування для реалізації
синхронності, але це не обов'язково. Кожне інтенсивне до CPU
завдання може надавати таку ж поведінку, як і блокування.
Взагалі, перевагу мають асинхронні API, тому що вони
гарантують, що система може просуватись. Актори є асинхронними
по природі: актор може просуватись після відправки
повідомлення, без очікування коли насправді відбудеться
доставка.
Неблокуючі
vs. блокуючі
Ми
кажемо про блокування,
якщо затримка одного потоку може невизначено затримати деякі
інші потоки. Гарним прикладом є ресурс, який може бути
використаний ексклюзивно одним потоком з використанням
взаємного виключення. Якщо потік утримує ресурс
невизначений час (наприклад, випадково увійшовши в
безскінчений цикл), інші потоки, що очікуватимуть ресурс, не
будуть просуватись. Навпаки, неблокуючий, означає,
що ніякий потік не може затримати інший на невизначений
час.
Неблокуючі
операції мають перевагу над блокуючими, тому що загальний
прогрес системи тривіально не гарантований, коли вона містить
блокуючі операції.
Глухий
кут (deadlock) vs. голодування (starvation) vs. живий кут
(live-lock)
Глухий кут виникає, коли
декілька учасників очікують один одного, щоб досягти
визначеного стану, в якому можна продовжувати далі. Жодний з
них не може просуватись, без того, щоб інший не досяг
визначеного стану (проблема "Catch-22"), що призводить до
зупинки всіх підсистем. Глухий кут близько пов'язаний з блокуванням,
тому що необхідною умовою є те, щоб потік міг затримати
просування інших потоків на невезначений час.
На
відміну випадку глухого кута, коли
учасники не можуть просуватись, під час голодування
учасники можуть прогресувати, але один або декілька -
ні. Типовий сценарій це випадок, коли діє природний алгоритм
планування, що обирає високоприоритетні завдання, скоріше,
ніж, низькоприоритетні. Якщо число надходящих
високоприоритетних завдань постійне і досить велике,
низькоприоритетні можуть ніколи не завершитись.
Живий кут подібний
до глухого
кута, бо
жодний з учасників не може прогресувати. Різниця, однак,
полягає в тому, що замість бути замороженими в стані
очікування, коли інші просунуться, учасники постійно змінюють
свій стан. Приклад цього сценарію, коли два учасника мають два
доступні ідентичні ресурси. Кожний з них намагається отримати
ресурс, але вони також перевіряють, чи інший також потребує
ресурс. Коли ресурс запитаний іншим, вони намагаються отримати
інший примірник ресурсу. В несчасливому випадку може статись,
коли два учасника "скачуть" між двома ресурсами, ніколи не
захвачуючи їх, але кожного разу поступаючись один одному.
Стан
перегонів
Ми
називаємо це станом перегонів,
коли припущення щодо впорядкованості набору подій може бути
зруйновані зовнішніми недермінованими ефектами. Стан
перегонів часто виникають, коли багато потоків мають загальний
змінний стан, та операції в потоці відповідно цього стану
можуть перемішуватись неочікуваним чином. Хоча це загальний
випадок, загальний стан не обов'язково призводить до стану
перегонів. Одним з прикладів може бути клієнт, що надсилає
невпорядковані пакети (наприклад, датаграми UDP) P1, P2 до сервера. Як пакети
можуть потенційно мандрувати через різні мережеві маршрути,
існує можливість, що отримає P2 раніше, та P1 після цього. Якщо
повідомлення не містять інфорамаці щодо порядку їх надсилання,
сервер не має можливості визначити, що вони надіслані в іншому
порядку. В залежності від значення пакетів, це може спричинити
стан перегонів.
Зауваження
Єдиною
гарантією, що провадить Akka щодо повідомлень, відісланних
між даною парою акторів, є те, що їх порядок завжди
зберігаєтсья. Дивіться Надійність
доставки повідомлень
Гарантування
відсутності блокування (умови прогресу)
Як
обговорювалось в попередніх розділах, блокування є небажаним з
декількох причин, включаючи загрозу мертвого куту, та
зменшеної пропускної спроможності системи. В наступному
розділі ми обсудимо різноманітні неблокуючі властивості з
різними потужностями.
Вільність
від очікування (Wait freedom)
Метод
є вільним
від очікування,
якщо кожний виклик гарантовано завершиться за скінчене
число кроків. Якщо метод визначено вільний від
очікування, число кроків має скінчену межу.
З
цього визначення слідує, що вільні від очікування методи
ніколи не блокуються, і, таким чином, глухий кут не виникає.
Додатково, позаяк кожний приймаючий участь може прогресувати
після скінченого числа кроків (коли виклик завершиться),
вільні від очікування методи вільні від голодування.
Вільність
від заморожування (Lock freedom)
Вільність від
заморожування є
слабкішою властивістю, ніж вільність від очікування.
В цьому випадку безскінчено часто деякі методи завершуються
за скінчене число кроків. Це визначення припускає, що глухий
кут неможливий для вільних від заморожування викликів. З
іншого боку, гарантії, що деякі виклики
завершуються в
скінчене число кроків не є достатньою гарантією, що всі
з них колись завершаться. Іншими словами, вільність
від заморожування не є достатньою гарантією від голодування.
Вільність
від захаращення (Obstruction-freedom)
Вільність від захаращення є найслабшою неблокуючою
гарантією, що обговорюється тут. Метод
називається вільним від захаращення, якщо є точка в часі,
після якої він виконується в ізоляції (інші потоки
не роблять кроків, тобто знаходяться в призупиненному
стані), він завершується в обмежене число кроків. Всі
об'єкти, вільні від заморожування, також захищені від
захаращення, але протилежне загалом невірно.
Оптимістичне управління
конкурентністю (Optimistic concurrency control, OOC)
часто також вільне від захаращення. Підхід OCC полягає в
тому, що кожний учасник намагається виконати свою операцію з
розділеним об'єктом, але якщо він визначає конфлікти з
іншими, він відкатує модифікації, та намагається знову
згідно деякого розкладу. Якщо настає точка в часі, коли один
з учасників є єдиним, хто родить спроби, операція буде
успішною.
Рекомендована
література
- The Art of Multiprocessor
Programming, M. Herlihy and N Shavit, 2008. ISBN
978-0123705914
- Java Concurrency in
Practice, B. Goetz, T. Peierls, J. Bloch, J. Bowbeer, D.
Holmes and D. Lea, 2006. ISBN 978-0321349606
Системи
акторів
Актори
- це об'єкти, що інкапсулюють стан та поведінку, вони
комунікують виключно через обмін повідомленнями, що розміщуються
в поштовій скринці отримувача. В цьому сенсі актори є найбільш
прямою формою об'єктно-орієнтовного програмування, але краще
дивитись на них як на особистості: коли рішення моделюється за
допомогою акторів, уявляйте їх як групу людей, та давайте їм
суб-завдання, розташовуйте їх функції в організаційну структуру
та думайте про те, як подолати відмову (все це з вигодами не
мати справу з дійсними людьми, що означає не мати потреби
звертати увагу на власний емоційний стан або моральні вади).
Результат може прислужитись як моральна підтримка для побудови
програмної реалізації.
Зауваження
ActorSystem
є важкою структурою, що розміщує 1…N потоків, так що створюйте
один примірник для кожного логічного застосування.
Ієрархічна
структура
Подібно
до економічної організації, актори природно формують ієрархії.
Один актор, що наглядає за окремою функцією в програмі, може
вирішити розділити свої завдання на меньші, більш керовані
шматки. Для ціх цілей він стартує дочірні актори, за якими він
наглядатиме. Хоча деталі супервізора пояснені тут,
ми сконцентруємося на підлеглих концепціях в цьому розділі.
Єдина передумова - це знати, що кожний актор має тільки один
супервізор, що також є актором, який його створив.
Квінтессенція
систем акторів в ціх завданнях, що поділяються та делегуються
нижче, доки вони не стануть досить малими, щоб бути
обробленими в одму місці. Таким чином не тільки самі завдання
стають більш зрозуміло структурованими, але також отримані
актори можуть бути визначені в термінах повідомлень, які вони
мають обробляти, як вони мають звичайно реагувати, та як
повинні оброблятись відмови. Якщо один актор не має змоги для
подолання окремої ситуації, він надсилає відповідне
повідомлення про відказ супервізору, покликаючи на допомогу.
Рекурсивна структура дозволяє обробити відказ на вищому
рівні.
Порівняйте
це з розшарованим дизайном програмного забезпечення, що легко
скочується до захисного програмування, ціллю якого є не
випустити назовні жодного відказу: якщо проблема доведена до
вірної особи, кращим рішенням може бути змога тримати все “під
ковдрою”.
Тепер
складність розробки такої системи в тому, як вирішити, хто
повинен наглядати за усім. Зрозуміло, що немає єдиного
найкращого рішення, але є декілька думок, що можуть бути
корисними:
- Якщо один актор керує тим,
що робить інший актор, надсилаючи суб-завдання, тоді
менеджер повинен наглядати за дочірнім актором. Причиною
є те, що менеджер знає, які типи збоїв очікуються, та як
обробляти їх.
- Якщо один актор переносить
дуже важливі дані (тобто його стан не можна втратити,
якщо цього можна уникнути), цей актор повинен віддавати
любі можливі загрозливі суб-завдання дочкам, яких він
наглядає, та обробляти їх збої відповідно. В залежності
від природи запитів, може бути кращим створити
нового актора для кожного запиту, що спрощує
керування станом для збору відповідей. Це відоме
як “Шаблон ядра помилок” (Error Kernel Pattern) з
Erlang.
- Якщо актор залежить від
іншого актора, на якого він переклав своє навантаження,
він повинен наглядати, щоб той актор був живий та діяв
до отримання завершального повідомлення. Це
відрізняється від супервізора, тому що нагляд частково
не має впливу на стратегію супервізора. Також слід
зауважити, що окремо сама функціональна залежність не є
критерієм для вирішення, де саме розташувати окремого
актора в ієрархії.
Звичайно,
завжди є виключення з ціх правил, але не важливо, чи ви
слідуєте правилам, чи ви їх порушуєте - в жодному разі ви
матиметиме на це привід.
Контейнер
конфігурації
Система
акторів, як асамблея колаборації акторів, є природним
пристроєм для керування розподіленими застосунками, як сервіси
планування, конфігурації, журналювання, etc. Декілька систем
акторів з різними конфігураціями можуть співіснувати в тій же
JVM без проблем, немає глобального розподіленого стану в
самомоу Akka. Поєднайте це з прозорою комунікацією між
системами акторів — на одному вузлі або через мережеве
з'єднання — щоб побачити, що системи акторів самі по собі
можуть використовуватись як будівельні блоки в функціональній
ієрархії.
Як
мають поводитись актори
- Актори повинні бути гарними
співробітниками: виконувати свою роботу ефективно, не
турбуючи будь-кого іншого без потреби, та запобігати
спотворення ресурсів. Перекладаючи на програмування це
означає обробляти події те генрувати відповіді (або інші
запити) в подія-рушійній манері. Актори не повинні блокувати
(тобто пасивно очікувати, займаючи потік) на деяких зовнішні
сутностях — що може бути блокування, мережевий сокет, таке
інше — за винятком коли цього не можна уникнути; в
останньому випадку дивіться нижче.
- Не передавати змінні об'єкти
між акторами. Щоб забезпечити це надавайте перевагу
незмінним повідомленням. Якщо інкапсуляція акторів
зруйнована через викриття іх змінного стану назовні, ви
приходите до земель звичайної конкурентності Java, з усіпа
недоліками.
- Актори зроблені бути
контейнерами для стану і поведінки, що означає не надсилати
поведінку в повідомленнях (що може бути спокусливим з
використанням замикань Scala). Один з ризиків є ненавмисне
поділення змінного стану між акторами, та це порушення
моделі акторів нажаль руйнує всі властивості, що роблять
програмування з акторами таким гарним досвідом.
- Актори вищого рівню є
внутрішньою частиною вашого ядра помилок, так що створюйте
їх помірковано, та схиляйтесь до справжні ієрархічних
систем. Це має переваги з точки зору обробки збоїв (з обох
боків, зважаючи на гранулярність конфігурації та
продуктивності), та це також зменьшує навантаження на
актора-охоронця, що є єдиною точкою спору в разі надмірного
використання.
Блокування
потребує уважного менеджменту
В
деяких випадках не можна уникнути виконання блокуючої
операції, тобто відправити потік в сон на невизначений термін,
очікуючи виникнення зовнішньої події. Прикладами є старі
драйвери RDBMS або API повідомлень, та грунтовна причина
типово така, що в основі лежить (мережевий) ввод-вивод. Коли
ви стикиєтесь з цім, ми можете бути спокушені прото огорнути
блокуючий виклик в Future та надалі робити з ним, але
ця стратегія дуже проста: ви, напевне, знайдете тонкі
місця або вичерпаєте ліміт пам'яті або потоків, коли
застосування виконуватиметься під збільшенним
навантаженням.
Неповний
перелік адекватних рішень “блокуючої проблеми” включає
наступні поради:
- Робіть блокуючий виклик в
акторі (або в наборі акторів, що керуються
маршрутизатором), переконавшись, що сконфігуровано пул
потоків, що або призначені для цієї цілі, або мають
достатній розмір.
- Робіть блокуючий виклик
в
Future,
провадячи вищий ліміт числа таких викликівв кожний
момент часу (надання необмеженого числа завдань такої
природи вичерпає запаси вашої пам'яті та потоків).
- Робіть блокуючий виклик
в
Future,
провадячи пул потоків з обмеженням числа потоків, що
відповідає апаратному забезпеченні, на якому виконується
застосування.
- Виділіть один потік для
управління набору блокуючих ресурсів (токий як NIO
селектор, що рухає декілька каналів) та відправляє події
по мірі виникнення як повідомлення акторів.
Перша
можливість особливо гарно підходить до ресурсів, що
однопоточні по своїй натурі, як указівники на бази даних, що
традиційно можуть одночасно виконувати тільки один окремий
запит, та використовують внутрішню синхронізацію для
забезпечення цього. Загальний шаблон є створення
маршрутизатора для N акторів, кожний з яких огортає одне
з'єднання з DB та обробляє запити, надіслані до
маршрутизатора. Потім число N мусить бути налаштовано для
найбільшої полоси пропускання, що буде варіювати в залежності
від того, яка DBMS розгорнута, та на якому обладнанні.
Зауваження
Конфігурація
пулів потоків я завданням, що краще делегувати Akka, просто
сконфігурувавши в application.conf та реалізувати через ActorSystem
Про
що вам не треба турбуватись
Система
акторів керує ресурсами, що сконфігуровані для використання,
щоб виконувати акторів, що вона містить. Може бути мільйони
акторів в одній такій системі, та по всьому мантра в тому, щоб
бачити їх як ряску з накладними розходами приблизно 300 байт
на примірник. Природно, що точний порядок, в якому
повідомлення оброляються в великих системах не може
конролюватись автором застосування, але цього і не було в
намірах. Відступіть та заспокойтесь, доки Akka візьме важку
роботу на себе.
Що
таке актор?
Попередний
розділ щодо Систем
акторів пояснив,
як актори формують ієрархії та є найменьшими блоками при
побудові застосувань. Цей розділ розглядає один такий актор в
ізоляції, пояснюючи концепції, які стануть вам в нагоді, коли
ви будете реалізовувати їх. it. Для більш глибокого посилання
з усіма деталями звертайтесь до Актори
(Scala) та Нетиповані
актори (Java).
Актор
є контейнером для стану,
поведінки, поштової скриньки,
дітей та стратегії супервізора.
Все це інкапсюловано за Посиланням
на актора (Actor Reference). Нарешті, треба розглянути,
що відбувається Коли
актор зевершує роботу.
Посилання
на актора
Як
деталізується нижче, об'єкт актора повинен бути захищений
від зовнішнього світу, щоб отримати переваги від моделі
акторів. Таким чином актори представлені загалу з
використанням посилань, що є об'єктами, що можуть бути
вільно передані та без обмежень. Цей поділ на внутрішній та
зовнішній об'єкти дозволяє прозорість для всіх бажаних
операцій: рестарт актора без потреби оновлення посилання
будь-де, розташування справжнього актора на віддалених
вузлах, надсилання повідомлень акторам з повністю різних
застосувань. Але найбільш важливий аспект в тому, що
неможливо подивитись на нутрощі актора та отримати його стан
ззовні, якщо тільки актор нерозсудливо не опублікує цю
інформацію самостійно.
Стан
Об'єкти
акторів будуть типово містити деякі змінні, які відображують
можливі стани актора, що можуть виникнути. Це може бути
машина станів в яіному вигляді (тобто з використанням
модуля FSM),
або це може бути лічильник, встановлені на слухачів,
очікувані запити, таке інше. Це те, що робить акторів
значущим, та вони мають бути захищені від пошкодження іншими
акторами. Гарна новина полягає в тому, що актори Akka
концептуально кожний має свій легковажний потік, що повністю
закрите від решти системи. Це означає, що замість мати
синхронизований доступ з використанням блокувань, ви можете
просто писати ваш код акторів, взагалі без хвилювання щодо
конкурентності.
За
лаштунками сцени Akka буде виконувати набори акторів на
наборах справжніх потоків, де зазвичай декілька акторів
поділяють один потік, та послідовні виклики одного актора
можуть призвести до обробки на різних потоках. Akka
забезпечує, що ці деталі реалізації не впливають на
однопоточність обробки стану актора.
Оскільки
внутрішній стан є життєдайним для роботи актора,
неузгодженний стан є фатальним. Таким чином, коли актор
схиблює та супервізор рестартує його, стан буде створений з
початку, як під час першого створення актора. Це утворює
можливість самолікування системи.
Опціонально,
стан актора может бути автоматично відтворенний до стану
перед рестартом, записуючи отримані повідомлення та
програваючи їх перед рестартом (дивіться
Постійність).
Поведінка
Кожного
разу, коли обробляється повідомлення, воно порівнюється з
поточною поведінкою актора. Поведінка означає функцію, що
означає дії, що виконуються як реакція на подію в данний
проміжок часу. Скажімо "переслати запит, якщо підлеглий
авторизований, та відмовити інакше". Ця поведінка може
змінюватись з часом, оскільки різні клієнти отримують
авторизацію з часом, або тому що актор може перейти в стан
"не обслуговується", та потім повернутись. Ці зміни
досягаються або кодуванням їх в змінних стану, що читаються
в логиці поведінки, або в самій функції, що може
замінюватись під час виконання, дивіться операції become та
unbecome.
Однак початкова поведінка, визначена під час конструювання
об'єкта актора, особлива в тому сенсі, що рестарт актора
буде скидати поведінку в цей первинний стан.
Поштова
скринька
Призначення
актора полягає в обробці повідомлень, та ці повідомлення
надсилаються до актора від іншого актора (або з за меж
системи акторів). Місце, що поєднує відправника та
отримувача є поштовою скринькою актора: кожний актор має
тільки одну поштову скриньку, куди всі відправники ставлять
в чергу свої повідомлення. Постановка в чергу відбувається в
порядку надсилання операцій, що означає, що повідомлення,
надіслані від різних акторів, не може мати різний порядок
під час виконання через наочну випадковість розподілення
акторів між потоками. Надсилання декількох повідомлень до
тієї ж цілі від того ж актора, з іншого боку, буде ставити
їх в чергу в тому ж порядку.
Є
різні реалізації поштових скриньок, з яких є вибір. По
замовчанню встановлена поштова скринька типу FIFO: порядок
повідомлень, що обробляється актором, співпадає з порядком
як вони ставились в чергу. Це зазвичай гарно по замовчанню,
але застосування можуть потребувати надати приоритет деяким
повідомленням, порівняно з іншими. В цьому випадку скринька
з приоритетами буде ставити в чергу не обов'язково в кінець,
але в позицію, що диктується приоритетом повідомлення,
можливо навіть на початок черги. При використанні такої
черги порядок повідомлень визначається алгоритмом черги, та,
загалом, не буде FIFO.
Важлива
можливість, якою Akka відрізняється від інших реалізацій
акторів, полягає в тому, що поточна поведінка повинна завжди
обробляти наступне повідомлення з черги, немає сканування
поштової скриньки в пошуках наступного співпадаючого
повідомлення. Збій при обробці повідомлення типово буде саме
збій, якщо ця поведінка не була перевизначена.
Діти
Кожний
актор потенційно є супервізором: якщо він створює дочірні
актори для делегації суб-завдання, він буде автоматично
наглядати за ними. Перелік дітей підтримується в контексті
актора, та актор має доступ до нього. Модифікації списку
виконуються створенням (context.actorOf(...))
або зупинкою (context.stop(child))
дітей, та ці дії відтворюються безпосередньо. Справжні дії
створення та завершення відбуваються за лаштунками сцени,
асинхронним чином, так що вони не "блокують" ваш
супервізор.
Стратегія
супервізора
Остання
частина актора - ща стратегія обробки збоїв його дітей.
Обробка збоїв потім виконується прозоро в Akka, застосовуючи
одну зі стратегій, описаних в Нагляд
та моніторинг
для кожного вхідного збою. Позаяк ця стратегія є
фундаментальною до того, як структурована система акторів,
вона не може бути змінена після створення актора.
Приймаючи
до уваги, що є тільки одна така ятратегія для кожного
актора, це означає, що коли різні стратегії застосовуються
для різних дітей актора, діти повинні бути згруповані по
проміжних супервізорах з відповідними стратегіями, вважаючи
за краще ще раз структурувати системи акторів згідно до
розподілення завдань на суб-завдання.
Коли
актор завершується
Коли
актор завершується, тобто дає збій в такий шлях, що не
оброблюється рестартом, завершує себе або зупиняється
супервізором, він вивільняє всі ресурси, скидає всі
повідомлення, що залишились в поштовій скринці, в системну
“скриньку мертіих повідомлень”, що буде передана до
EventStream як DeadLetters. Потім поштова скринька
замінюється в посиланні на актора на системну поштову
скриньку, переводячи всі нові повідомлення до EventStream як
DeadLetters. Це, однак, зроблено на основі кращого з
можливого, так що не розраховуйте на це, щоб конструювати
“гарантовану доставку”.
Причина
не просто мовчки складати повідомлення підказана нашими
тестами: ми реєстрували TestEventListener на шині подій, до
якої надсилались мертві повідомлення, та він журналював
попередження для кожного отриманого мертвого повідомлення —
це було дуже корисно для дешифрування тестових збоїв більш
швидко. Вважається, що ця можливість може також
використовуватись для інших цілей.
Нагляд
та моніторинг
Ця
глава описує концепції та підгрунтя нагляду (супервізора),
запропонованих примітивів, та їх семантик. Для детального
опису, як це транслюється в реальний код, звертайтесь до
відповідних глав Scala та Java API.
Що
значить нагляд супервізора
Як
описано в Системах
акторів
нагляд визначає відносини залежності між акторами:
супервізор делегує завдання підлеглим, і, таким чином, має
відповідати на їх збої. Коли підлеглий визначає помилку
(тобто викликає виключення), він призупиняє себе та своїх
підлеглих, та відсилає повідомлення своєму наглядачу,
сповіщающи про збій. В залежності від природи збою
супервізор має вибір з наступних чотирьох опцій:
- Відновити підлеглого,
зберігаючи його акумульований внутрішній стан
- Рестартувати підлеглого,
очищуючи його акумульований внутрішній стан
- Зупинити підлеглого
назавжди
- Ескалувати збій, тобто
самому об'явити збій
Важливо
завжди дивитись на актора як на частину ієрархії
супервізоров, що пояснює існування четвертого вибору
(позаяк супервізор також підлеглий для іншого супервізора
рівнем вище), та має вплив на перші три: відновлення
роботи актора відновлює всіх його підлеглих, рестарт
актора рестартує всіх його підлеглих (але дивіться нижче
щодо деталей), подібно завершення актора також завершує
всіх його підлеглих. Треба зазначити, що по замовчанню
поведінка перехоплювача preRestart класу Actor є завершення всіх дітей
перед рестартом, але цей перехоплювач може бути
перевизначений; рекурсивний рестарт стосується до
всіх дітей, що залишились після того, як цей перехоплювач
буде виконаний.
Кожний
супервізор сконфігуровано за допомогою функції, що
транслює всі можливі причини збоїв (тобто виключення) в
один з чотирьох наданих вище виборів; важливо, що ця
функція не приймає ідентифікатор збійного актора як
вхідний параметр. Досить просто навести приклади структур,
де це може не виглядати досить гнучким, тобто бажане мати
різні стратегії стосовно різних підлеглих. На цей час
життєво важно зрозуміти, що нагляд призначений для
формування рекурсивної структури обробки збоїв. Якщо ви
спробуєте зробити дуже багато на одному рівні, це буде
складним пояснити, оскільки рекомендованим шляхом в цьому
випадку додати рівень нагляду.
Akka
реалізує специфічну форму з назвою “батьківський нагляд”.
Актори можуть бути створені тільки іншими акторами — де
актор вищого рівня провадиться бібліотекою — та
кожний створений актор наглядається його батьківстким
актором. Це обмеження робить формування ієрархії
акторів-супервізоров неявним, та заохочує вражаючі
рішення. Треба зазначити, що це також гарантує, що актори
не можуть загубитись, або бути приєднані до супервізорів
ззовні, що могло в іншому випадку захопити їх зненацька.
На додаток, це In addition, це дає природну та чисту
процедуру завершення для (суб-дерев) застосувань акторів.
Попередження
Пов'язані
з наглядом комунікації батько-дитина відбуваються за
допомогою спеціальної системи повідомлень, що мають свої
власні поштові скриньки, відмінні від користувацьких
повідомлень. Це передбачає, що події, пов'язані з
наглядом, не є детерміновано впорядкованими відносно
звичайних повідомлень. Загалом користувач не може
впливати на порядок звичайних повідомлень та повідомлень
про збої. Щодо деталей та приклада дивіться розділ
Дискусія: порядок повідомлень.
Високорівневі
супервізори
Система
акторів буде під час створення стартувати щонайменьше три
актори, показані на малюнку вище. Для додаткової
інформації щодо наслідків для шляхів акторів
дивіться Сфери
вищого рівня для шляхів акторів
/user:
Актор-вартівник
Актор,
з яким напевне відбувається найбільше взаємодій, є
батьківський для всіх користувацьких акторів, вартівник
на ім'я "/user".
Актори, створені з використанням system.actorOf(), є діти цього
актора. Це означає, що коли цей вартівник завершується,
всі нормальні актори в системі також завершуються. Це
також означає, що стратегія супервізора вартівника
визначає, як відбувається нагляд за високорівневими
нормальними акторами. Починаючи з Akka 2.1 можливо
сконфігурувати це, використовуючи налаштування
akka.actor.guardian-supervisor-strategy,
що сприймає повністю кваліфіковане ім'я класу SupervisorStrategyConfigurator.
Коли вартівник ескалує збій, відповідь кореневого
вартівника буде завершити вартівника, що ефективно
завершить цілу систему акторів.
/system:
Системний вартівник
Спеціальний
вартівник був введений, щоб досягти впорядкованої
послідовністі завершення, коли журналюванню залишається
активною, тоді як всі нормальні актори завершуються,
навіть вважаючи, що журнал самий реалізовано з
використанням акторів. Це втілено за допомогоюсистемного
вартівника, що наглядає за користувацьким наглядачем, та
ініціює своє власне завершення при отриманні
повідомлення
Terminated.
Високорівневі системи акторів наглядаються з
використанням стратегії, що буде рестартувати
безскінчено для всіх типів Exception, виключаючи ActorInitializationException та
ActorKilledException,
що будуть завершати дитя. Всі інши підійняті виключення
будуть ескалуватись, що завершить всю систему акторів.
/:
Кореневий вартівник
Кореневий
вартівник є прадідом всіх так званих "високорівневих"
акторів, та наглядає за всіма особливими акторами,
переліченими в Високорівневі
сфери та шляхи акторів
з використанням SupervisorStrategy.stoppingStrategy,
чиє призначення завершити дітей за виникнення любого
типу Exception.
Всі інші будуть ескаловані.. але куди? Оскільки любий
справжній актор має наглядача, наглядач кореневого
актора не може бути дійсно актором. Та оскільки це
означає, що він “за межами бульбашки”, він називається
“той, хто мандрує по бульбашках простору та часу”. Це
синтетичний ActorRef, що ефективно завершує
свого дитя при перших натяках на проблеми, та
встановлює статус для системи акторів isTerminated в
true, як тільки кореневий
вартівник повністю завершиться (всі діти
рекурсивно завершаться).
Що
означає рестарт
Коли
ми зустрічаємось з актором, що схибив під час обробки
певного повідомлення, причини збою підпадають в три
категорії:
- Систематичні помилки
(програмування) для окремого отриманного повідомлення
- (Перехідні) відмови
деякого зовнішнього ресурсу, що використовується в
процесі обробки повідомлення
- Зруйнований внутрішній
стан актора
Якщо
збій не був окремо розпізнаний, не можна виключати третю
причину, що призводить до висновку, що внутрішній стан
треба очистити. Якщо супервізор вирішає, що його інше дитя
або він сам не причасний до пошкодження — наприклад,
через свідоме використання помилки шаблону ядра
помилки — та, таким чином, краще рестартувати дитя.
Це зводиться до створення нового примірнику підлеглого
класу Actor та заміщення збійного
примірнику на свіжий в дитячому ActorRef;
можливість зробити це є однією з причин енкапсуляції
акторів в спеціальних посиланнях. Потім новий актор
відновлює обробляти свою поштову скриньку, що означає, що
рестарт не видимий за лаштунками самого актора, з помітним
виключенням, що повідомлення, під час якого трапився збій,
не буде оброблене повторно.
Точна
послідовність подій на протязі рестарту наступна:
- призупинити актора (що
означає, що він не буде звичайно обробляти повідомлення
до відновлення), та рекурсивно призупинити всіх його
дітей
- викликати перехоплювач
preRestart старого примірнику
(по замовчанню надсилає запити на завершення до всіх
дітей та викликає postStop)
- зачекати (використовуючи
context.stop()),
доки дійсно завершаться всі діти, для яких був запит на
завершення під час
preRestart;
це — як всі операції з акторами — не блокує,
повідомлення завершення авд останнього вбитого дитя буде
ефективно просувати до наступного кроку
- створити примірник нового
актора, викликавши оригінально запроваджену фабрику ще
раз
- викликати
postRestart на новому примірникі
(що по замовчанню також викликає preStart)
- надіслати запит на рестарт
до всіх дітей, що не вбиті під час кроку 3; рестартовані
діти будуть слідувати тій же процедурі ресурсивно, від
кроку 2
- відновити актора
Що
означає моніторинг життєвого циклу
Зауваження
Моніторинг
життєвого циклу в Akka звичайно відомий як DeathWatch
До
контрасту зі спеціальними відносинами між батьками та
дітьми, описаними вище, кожний актор може моніторити
кожного іншого актора. Оскільки актори з'являються від
створення та рестарти не видимі за межами задіяних
супервізорів, єдиною зміною стану для моніторингу є
перехід від живого до мертвого. Таким чином моніторинг
використовується для пов'язання одного актора до іншого,
так що він може відреагувати на його завершення, на
відміну від супервізора, що реагує на збої.
Моніторинг
життєвого циклу реалізоано з використанням
повідомлення Terminated, до буде отримано
актором що моніторить, та поведінка за замовченням є
викликати спеціальне виключення
DeathPactException, якщо немає іншої
обробки. Щоб почати прослуховувати повідомлення Terminated,
викличте ActorContext.watch(targetActorRef).
Щоб перестати слухати, викличтеActorContext.unwatch(targetActorRef).
Одна важлива властивість полягає в тому, що ці
повідомлення будуть доставлені безвідносно до порядку, в
якому відбувались запити на моніторинг та завершення
цілей, тобто ви все ще отримаєте повідомлення, навіть якщо
на час реєстрації ціль вже була мертвою.
Моніторинг,
зокрема, корисний, якщо супервізор не може просто
рестартувати своїх дітей, та має завершити їх, тобто в
випадку помилок під час ініціалізації актора. В цьому
випадку він повинен моніторити ціх дітей, та перестворити
їх, або запланувати собі повторити це на пізніший
час.
Інше
загальне застосування - це коли актор потребує зхибити
за відсутності зовнішнього ресурсу, що може також
бути одним з його дітей. Якщо третя сторона завершує дитя
за допомогою методу
system.stop(child), або надсилаючи PoisonPill,
це також може вплинути на супервізор.
Відкладені
рестрти з шаблоном BackoffSupervisor
Провадячи
вбудований шаблон akka.pattern.BackoffSupervisor, актор реалізує так
звану стратегію експотенційної
затримки нагляду, що може використоуватись для
пожиттєвого нагляду актора, та коли він завершиться,
спробувати стартувати його знову, кожного разу з більшим
часом затримки між ціма рестартами.
Цей
шаблон корисний, коли запущений актор схиблює через те,
що деякий зовнішній ресурс не доступний, та нам треба
отримати деякий час для того, щоб стартувати знову. Один
з первинних прикладів, коли це корисне, це коли
схиблює PersistentActor зі збоєм
постійності - що вказує, що база даних може бути
вимкнена або перевантажена. В такій ситуації має більше
сенсу надати трохи більше часу для відновлення, перед
тим як постійний актор буде рестартований.
Наступний
клаптик Scala показує, як створювати супервізор з
затримкою, що буде стартувати даний echo-актор з
наростаючими інтервалами в 3, 6, 12, 24, та, нарешті, 30
секунд:
- val childProps = Props(classOf[EchoActor])
-
- val supervisor = BackoffSupervisor.props(
- childProps,
- childName = "myEcho",
- minBackoff = 3.seconds,
- maxBackoff = 30.seconds,
- randomFactor = 0.2) // додає 20% "шуму" щоб трохи варіювати інтервали
-
- system.actorOf(supervisor, name = "echoSupervisor")
Це
еквівалентно такому коду Java:
- import scala.concurrent.duration.Duration;
- final Props childProps = Props.create(EchoActor.class);
-
- final Props supervisorProps = BackoffSupervisor.props(
- childProps,
- "myEcho",
- Duration.create(3, TimeUnit.SECONDS),
- Duration.create(30, TimeUnit.SECONDS),
- 0.2);
-
- system.actorOf(supervisorProps, "echoSupervisor");
Тут
randomFactor використовується для
довання трохи більшої варіації до інтервалів затримки,
що дуже рекомендоване, щоб уникнути ситуації,
коли дкілька акторів рестартують точно в тій же точці
часу, наприклад, оскільки вони зупинились через
розділений ресурс, такий, як база даних, що впала, та
рестарт через той же сконфігурований інтервал. Додавання
випадковості до інтервалів рестарту акторів призведе то
того, що вони будуть стартувати в трохи інший час, таким
чином запобігаючи великих піків трафіку, націленого на
розподілену базу даних, або на інший ресурс, з яким їм
треба з'єднатись.
Стратегія
кожний-за-себе vs. всі-за-одного
Є
дві класичні класи стратегій супервізора, що ідуть з
Akka: OneForOneStrategy та AllForOneStrategy.
Обоє сконфігуровані з відображенням з типу виконання на
директиви нагляду (дивіться вище
above),
та обмежують, як часто дитині дозволяється схибити, перш
ніж вона буде завершена. Різниця між ними така, що перша
застосовує отриману директиву тільки до дитини, що
схибила, тоді як друга застосовує також до всіх сестер.
Зазавичай ви повинні використовувати OneForOneStrategy,
що також є по замовчанню, якщо явно не вказане інше.
Стратегія AllForOneStrategy стосовна в випадках,
коли гурт дітей мають такий тісний зв'язок між собою, що
збій одного впливає на функцюювання інших, тобто вони
нерозривно пов'язані. Оскільки рестарт не очищує поштову
скриньку, часто краще завершити дітей під час збою, та
явно перестворити їх з боку супервізора (наглядаючи
за життєвим циклом дітей); інакше ви маєте бути впевненим,
що для кожного з акторів не виникає проблем, коли він
отримав повідомлення, що було поставлене в чергу перед
рестартом, але оброблене після.
Звичайно,
зупинка дитини (тобто не відповідь на збій) не буде
автоматично завершувати інших дітей в стратегії
всі-за-одного; це може бути просто зроблене, наглядаючи за
їх життєвим циклом: якщо повідомлення
Terminated не оброблене
супервізором, він підіймає DeathPactException, що (в
залежності від свого супервізора) буде рестартувати їх, та
дія по замовчанню preRestart буде завершувати дітей.
Звичайно, це може бути оброблено також явно.
Занотуйте
будь ласка, що створення одноразових акторів в супервізорі
всі-за-одного тягне за собою те, що збої, ескаловані
тимчасовим актором, будуть впливати на всі постійні
актори. Якщо це не бажано, встановіть проміжний
супервізор, ; це може бути дуже просто зроблене,
декларуючи маршрутизатор розміру 1 для робітника,
дивіться: Маршрутизація або тут:
Маршрутизація.
Посилання
на акторів, шляхи та адреси
Ця
глава описує, як актори ідентифікуються, та розміщуються
в, можливо розподіленій, системі акторів. Це пов'язане з
центральною ідеєєю, що Системи
акторів
формують внутрішні ієрархії нагляду, так само, як
те, що комунікації між акторами прозорі з точки зору їх
розташування між декількома вузлами мережі.
Малюнок
вище показує відносини між найбільш важливими сутностями в
системі акторів. Деталі читайте далі.
Що
таке посилання на актора?
Посилання
на актора є підтипом ActorRef,
чиє головне призначення підтримувати надсилання
повідомлень до актора, якого воно представляє. Кожний
актор має доступ до свого канонічного (локального)
посилання через поле self;
це посилання також включене як посилання на відправника
для всіх повідомлень, надісланих іншим акторам.
Навпаки, впродовж обробки повідомлення актор має
доступ до посилання, що представляє відправника
поточного повідомлення, через метод
sender.
Є
декілька різних типів посилань, що підтримуються, в
залежності від конфігурації системи акторів:
- Чисто локальні посилання
акторів використовуються системами акторів, які не
сконфігуровані для підтримки мережевих функцій. Ці
посилання акторів не будуть функціонувати, якщо будуть
відіслані через мережеве з'єднання до віддаленої JVM.
- Локальні посилання
акторів, коли дозволені віддалені, використовуються
системами акторів, що підтримують мережеві функції для
ціх посилань, але ці посилання представляють акторів
на тій же JVM. Щоб бути доступними при надсиланні до
інших вузлів мережі, ці посилання включають протокол
та інформацію про віддалене адресування.
- Є підтип локальних
посилань акторів, що використовуються для
маршрутизаторів (тобто актор з підмішаним
трейтом
Router).
Їх
логічна структура такаж сама, як для вищезгаданих
локальних посилань, але надсилання повідомлення до них
доставляється напряму одному з їх дітей.
- Віддалені посилання
представляють акторів, що доступні з використанням
віддалених комунікацій, тобто надсилання повідомлень
до них буде прозоро серіалізувати повідомлення, та
надсилати їх на віддалену JVM.
- Є декілька спеціальних
типів посилань акторів, що поводяться як локальні для
всіх практичних цілей:
PromiseActorRef є спеціальним
представленням Promise для цілей бути
завершеним через відповідь від актора. akka.pattern.ask створює ці
посилання акторів.
DeadLetterActorRef є реалізацією по
замовчанню для сервісу мертвих повідомлень, до
якого Akka направляє всі повідомлення,
ції призначення завершились або не існували.
EmptyLocalActorRef це те,
що повертає Akka, коли бачить неіснуючий шлях
локального актора: це еквівалентно до DeadLetterActorRef,
але він зберігає свій шлях, так що Akka може
надіслати його по мережі та порівняти його з
іншими існуючими посиланнями акторів для цього
шляха, деякі з яких можуть бути отримані до того,
як актор помер.
- та ще є декілька
одноразових внутрішніх реалізацій, які ви насправді
ніколи не побачите:
- Є посилання актора,
що не представляє актора, але діє тільки як
псевдо-супервізор для кореневого вартівника, ми
називаємого його “той, хто мандрує по бульбашках
простору та часу”.
- Перший сервіс
журналювання, запущений перед справжнім запуском
потужностей, що створюють акторів, є фальшивим
актором, що сприймає повідомлення журналювання та
друкує їх прямо до стандартного виводу; це
Logging.StandardOutLogger.
Що
таке шлях актора?
Оскільки
актори створюються в суворо ієрархічний спосіб, існує
унікальна послідовність імен акторів, шр рекурсивно
слідує зв'язкам супервізорів між дітьми та батьками, все
далі до кореня системи акторів. Цю послідовність можна
розглядатись як вкладені теки файлової системи, тому ми
застосовуємо ім'я “шлях” для посилання на них. Як і в
деяких реальних файлових системах, є також “символічні
зв'язки”, тобто один актор може бути досяжний з
використанням більше ніж одного шляху, але всі крім
одного включають деяку трансляцію, що відокремлює
частину шляху від дійсної лінії супервізорів-предків
актора; ці подробиці описані в підрозділах нижче.
Шлях
актора складається з якоря, що ідентифікує систему
акторів, за яким слідує поєднання елементів шляху, від
кореневого вартівника до вказаного актора;
елементи шляху є іменами перейдених акторів, та
розділені косими рисками.
Яка
відмінність між посиланням на актора та його шляхом?
Посилання
на актора визначає одного актора, та життєвий цикл
посилання співпадає з життєвим циклом актора; шлях
актора представляє ім'я, що може відповідати або не
відповідати актору, та самий шлях не має життєвого
циклу, він ніколи не стає недійсним. Ви можете
створити шлях актора без створення актора, але ви не
можете створити посилання актора без створення
відповідного актора.
Ви
можете створити актора, завершити його, та потім
створити нового актора з тим же шляхом. Новий
створений актор є новою інкарнацією актора. Це не той
самий актор. Посилання на стару інкарнацію актору не
буде дійсною для нової інкарнації. Повідомлення,
надіслані старому посиланню актора не будуть
доставлені новій інкарнації, навіть зважаючи, що вони
мають один шлях.
Якори
шляха актора
Кожний
шлях актора має компонент адреси, що описує протокол
та розміщення, по якому можна досягти відповідного
актора, за яким слідують імена акторів в ієрархії від
кореня вгору. Ось приклади:
- "akka://my-sys/user/service-a/worker1" // повністю локальний
- "akka.tcp://my-sys@host.example.com:5678/user/service-b" // віддалений
Тут akka.tcp є віддалений
транспорт по замовчанню для реліза 2.2; інші
транспорт під'єднуються. Віддалений вузол, що
використовує UDP, має бути доступний з
використанням akka.udp.
Інтерпретація вузла та порта (як host.example.com:5678 в прикладі)
залежить від використовуваного транспортного
механізму, але він має дотримуватись структурним
правилам URI.
Логічні
шляхи акторів
Унікальний
шлях, що отриманий від слідування від батьківського
супервізора в напрямку кореневого вартівника
називається логічним шляхом актору. Цей шлях точно
співпадає з порядком створення акторів, так що від
повністью детермінистичний, як тільки встановлена
віддалена конфігурація системи акторів (да за
допомогоюї її адресний компонент шляху).
Фізичні
шляхи акторів
В
той час, як логічні шлихи акторів описують
функціональне розташування в одній системі акторів,
базоване на конфігуації віддалене розвертання означає,
що актор може бути створений на вузлах мережі, що не
співпадають з вузлом батьківського актора, тобто в
іншій системі акторів. В цьому випадку слідування
шляхом актора від кореневого вартівника нагору
завершиться мандрівкою по мережі, що є коштовною
операцією. Таким чином, кожний актор також має
фізичний шлях, починаючи з кореневого вартівника
системи акторів, де дійсно знаходиться об'єкт актора.
Використовуючи цей шлях як посилання на відправника,
при запиті інших акторів, дозволить їм відповідати
прямо актору, мінімізуючи затримки, викликані
машрутизацією.
Один
важливий аспект полягає в тому, що цей фізичний шлях
актора ніколи не перетинає декілька систем акторів або
JVM. Це означає, що логічний шлях (ієрархія
супервізорів) та логічний шлях (розвертання актора)
для актора може відрізнятись, якщо один з предків
наглядається віддалено.
Як
отримати посилання на акторів?
Є
дві головні категорії, до яких відносяться методи
отримання посилань на акторів: створенням акторів, або
шукаючи їх, де перша функціональність іде в двох
варіантах: створення посилань актора з конкретного шляху
актора, за запит логічної ієрархії актора.
Створення
акторів
Система
акторів типово починається зі створення акторів від
актора-вартівника, з використанням метода ActorSystem.actorOf,
та потім використовувати ActorContext.actorOf від
створеним акторів для відтворення дерева акторів. Ці
методи повертають посилання на тольки но створеного
актора. Кожний актор має прямий доступ (через ActorContext)
для посилання на його батька, самого його, та його
дітей. Ці посилання можуть бути надіслані через
повідомлення іншим акторам, дозволяючи їм відповідати
напряму.
Пошук
акторів по конкретному шляху
На
додаток посилання акторів можуть бути знайдені з
використанням метода ActorSystem.actorSelection.
Може бути використана селекція для комунікації з
вказаним актором, та актор, відповідаючий до селекції
переглядається, коли доставляється кожне повідомлення.
Для
отримання ActorRef, що прив'язаний до
життєвого циклу окремого актору, вам треба надіслати
повідомлення, таке, як вбудоване
повідомлення Identify,
до актора, на використовувати посилання sender() відповіді від
актора.
Абсолютні
vs. відносні шляхи
На
додаток до ActorSystem.actorSelection є також ActorContext.actorSelection,
що доступний в кожному актору як context.actorSelection.
Це дає селекцію актора здебільше подібно до його
двійника на ActorSystem,
але замість пошуку по шляху догори, від кореня
дерева актора, він починає з поточного актора.
Елементи шляху, що складаються з двох крапок ("..")
можуть використовуваться для доступу до
батьківського актора. Ви можете, наприклад,
надіслати повідомлення окремому рідному брату (або
сестрі):
- context.actorSelection("../brother") ! msg
Абсолютний
шлях, може, звичайно, також бути знайдений в
контексті звичайного шляху, тобто наступне теж буде
робити як очікується:
- context.actorSelection("/user/serviceA") ! msg
Запит
логічної ієрархії актора
Оскільки
система акторів формує ієрархію, подібну до файлової
системи, порівняння шляхів можливе в той же спосіб, як
підтримується в оболонці Unix: ви можете замінити
(частину) елементів імені шляху на загальні символи («*»
та «?») для
формулювання селектора, що може співпадати з нелем або
більше акторів. Оскільки результат не є єдиним
посиланням актора, він має інший тип ActorSelection, та не підтримує
весь набір операцій, що й ActorRef.
Селекції можуть бути сформульовані з використанням
методів ActorSystem.actorSelection та ActorContext.actorSelection, та підтримують
надсилання повідомлень:
- context.actorSelection("../*") ! msg
буде
надсилати msg
всіб братам всім сестрам, включаючи поточного актора.
Як і для посилань, отриманних за допомогою actorSelection,
виконується перебіг ієрархії, щоб виконати надсилання
повідомлення. Позаяк точний набір акторів, що
співпадають з селектором, може змінюваться навіть під
час того, як повідомлення мандрує до отримувачів, не
видається можливим наглядати за селекцією щодо змін
життездатності. Щоб зробити це, розрішіть непевність,
надсилаючи запити та збираючи всі відповіді, виділяючи
посилання на відправника, та потім наглядаючи за всіма
виявленими конкретними акторами. Ця схема розрішення
селекції може бути покращена в майбутньому релізі.
Підсумок: actorOf vs. actorSelection
Зауваження
Дія
перелічених описаних селекторів можна підсумувати та
просто запам'ятати наступним чином:
actorOf тільки створює
нового актора, та він створює його як прямого
потомка контексту, в якому викликається цей
метод (що може бути любим актором або
системою акторів).
actorSelection тільки шукає
існуючих акторів, куди доставляються
повідомлення, тобто не створює акторів,
або перевіряє існування акторів при створенні
селекції.
Рівність
посилань та шляхів акторів
Рівність
при порівнянні ActorRef має на увазі,
що ActorRef відповідає цільовій
інкарнації актора. Два посилання актора рівні при
порівнянні, коли вони мають той же шлях та вказують на
ту ж саму інкарнацію актора. Посилання, що вказує на
завершеного актора, не дає рівності при порівнянні з
посиланням, що вказує на іншого (перествореного) актора
з тим же шляхом. Зауважте, що рестпрт актора, викликаний
збоєм, все ще означає, що це та ж інкарнація актора,
тобто рестарт не є видімим для споживача ActorRef.
Якщо
вам треба відсліджувати посилання на актора в колекції,
та не важлива точна інформація актора, ви можете
використовувати ActorPath як ключ,
оскільки ідентифікатор цільового актора не приймається
до уваги при порівнянні шляхів акторів.
Повторне
використання шляхів акторів
Коли
актор завершений, його посилання буде вказувати на
мартву поштову скриньку, DeathWatch буде публікувати
його фінальне перетворення, та загалом не очікується, що
він знову поверненться до життя (оскільки життєвий цикл
актора не дозволяє це). Доки можливе створити актора в
пізднішій час з ідентичним шляхом — просто через те, що
неможливо змусити зворотнє без підтримки набору всіх
будь коли створених акторів — це не гарна практика:
повідомлення, надіслані з actorSelection до актора,
який “died” раптово починає робити знову, але без жодних
гарантій порядку між цією переміною та жодною іншою
подією, оскільки новий мешканець шляху може отримувати
повідомлення, що були призначені до попереднього
орендара.
Може
бути вірною річчю робити це в дуже особливих випадках,
але переконайтесь, що обмежуєте таку обробку до
супервізора, оскільки це єдиний актор, що може надійно
визначити належну дерегістрацію імені, перед якою
створення нової дитини буде схиблювати.
Це
також може знадобитись під час тестування, коли ціль
тесту залежить від спроможності створити примірник по
заданому шляху. В цьому випадку краще обманути його
супервізор, так що він буде пересилати повідомлення
Terminated в потрібну точку тестової процедури,
дозволяючи останньому очікувати належну дерегістрацію
імені.
Взаємодія
з віддаленим розгортанням
Коли
актор створює дитя, той, хто розгортає систему акторів,
буде приймати рішення, чи новий актор залишається на тій
же JVM, або на іншому вузлі. В другому випадку створення
актора буде перемикатись чере мережеве з'єднання, щоб
воно відбувалось в іншій JVM, та, відповідно, в іншій
системі акторів. Віддалена система буде роміщувати
нового актора нижче спеціального шлязу, зарезервованого
для цього призначення, та супервізор нового актора буде
посилання віддаленого актора (представляючи того актора,
що викликав його створення). В цьому випадку context.parent (посилання
супервізора) та context.path.parent (батьківський
вузол в шляху актора) не представляють того ж актора.
Однак пошук імені дитини в супервізорі буде знаходити
його на віддаленому вузлі, зберігаючи логічну структуру,
тобто потім надсилаючи на нерозрішене посилання актора.

Для
чого використовується адресна частина?
Коли
посилання актора надсилається по мережі, воно
представлене як шлях. Таким чином, шлях мусить повністю
кодувати всю інформацію, потрібну для надсилання
повідомлень потрібному актору. Це досягаєтсья завдяки
кодуванню протокола, вузла та порта в адресній частині
рядка шляху. Коли система акторів отримує шлях актора
від віддаленого вузла, вона перевіряє, чи адреса шляху
співпадає з адресою системи акторів, в якому випадку він
буде розрішений до локального посилання актора. Інакше
він буде представлений віддаленим посиланням
актора.
Сфери
високого рівня для шляхів актора
В
корені шляху ієрархії шляху розташований кореневий
вартівник, над яким знаходяться всі інші актори; його
ім'я "/".
Наступний рівень складається з наступного:
"/user" актор-вартівник для
всіх акторів, створених користувачем на верхньому
рівні; актори, створені з використанням
ActorSystem.actorOf знаходяться під цім
шляхом.
"/system" є актор-вартівник
для всіх акторів, створених
системою на вищому рівні, тобто слухачів
журналювання, або акторів, що автоматично
розгортаються конфігурацією при запуску системи
акторів.
"/deadLetters" є актор мертвих
листів, що є місцем, куди завертають всі повідомлення,
надіслані до неіснуючих акторів (на
основі кращої спроби: повідомлення можуть загубитись
навіть на локальній JVM).
"/temp" є вартівником для
швидко-живучих створених системою акторів, тобто
тих, що використовуються в реалізації ActorRef.ask.
"/remote" є штучним шляхом,
під яким знаходяться всі актори, чиї супервізори
є посиланнями на віддалених акторів
Потреба
структурувати простір імен для акторів, як це, постає з
центральної та дуже важливої цілі: все в ієрархії є
актором, та всі актори функціонують в той же спосіб.
Оскільки ви можете не тільки шукати акторів, яки ви
створюєте, в можете також шукати системного вартівника,
та надсилать йому повідомлення (яке він буде
покірно відкидати в цьому випадку). Цей потужний
принцип означає, що немає особливостей, які треба
пам'ятати, що робить цілу систему більш однорідною та
послідовною.
Якщо
ви бажаєте прочитати більше щодо високорівневої
структури системи акторів, погляньте на Супервізори
вищого рівня.
Прозорість
розташування
Попередній
розділ описує, як використовуються шляхи акторів, що
дозволяє прозорість розташування. Це спеціальна
можливість потребує дещо більшого пояснення, оскільки
пов'язаний термін “прозоре віддалення” використовувалось
досить різноманітно в контексті мов програмування,
платформ та технологій.
Розподілений
по замовчанню
Все
в Akka розроблене для роботи в розподіленій установці:
всі інтеракції акторів використовують чисту передачу
повідомлень, та все є асинхронним. Цей підхід був
обраний, щоб впевнитись, що всі функції доступні
однаково при роботі на одній JVM, або на кластері з
сотен машин. Ключ для впровадження цього це перехід
від віддаленого до локального шляхом оптимізації,
заміть того, щоб намагатись пройти від локального до
віддаленого, шлягом узагальнення. Дивіться цей
класичний папір для детальної дискуссії, чому
другий підхід приречений на невдачу.
Шляхи,
якими руйнується прозорість
Що
є вірним для Akka, не обов'язково вірне для
застосування, що використовує його, оскільки розробка
для розподіленого виконання накладає деякі обмеження
того, що можливе. Найбільш очевидним є те, що всі
повідомлення, надіслані по дроті, повинні бути
серіалізуємі. Хоч трохи менш очевидно, це включає
замикання, що використовуються як фабрики акторів
(тобто в Props),
якщо актор буде створюватись на віддаленому вузлі.
Іншим
наслідком є те, що все має бути готове до того, що всі
взаємодії будуть повністю асинхронним, що в
комп'ютерній мережі може означати, що похід
повідомлення до отримувача може зайняти декілька
хвилин (в залежності від конфігурації). Це також
означає, що вірогідність втрати повідомлення значно
вища, ніж на одній JVM, де воно близьке до нуля (але
все ще без твордих гарантій!).
Як
робить віддалення?
Ми
довели ідею прозорості до межі в тому сенсі, що майже
немає API для прошарку віддалення в Akka: це повністю
залежить від конфігурації. Просто напишіть ваше
застосування відносно до принципів, змальованих в
попередніх розділах, потім задайте віддалене
розгортання дерев акторів в файлі конфігурації. Таким
чином ваше застосування може бути маштабоване без
потреби торкатися коду. Єдина частина API, яка має
програмний вплив на віддалене розгортання, це те, що Props містить поле, що
може бути встановлене в певний примірник Deploy;
це матиме той же ефект, що і покласти еквівалентне
розгортання в файл конфігурації (якщо задані обоє,
файл конфігурації перемагає).
Точка-точка
vs. клієнт-сервер
Akka
Remoting є модулем комунікації для поєднання систем
акторів в стилі точка-точка, та є основою для Akka
Clustering. Розробка віддалення рухається двома
(пов'язаними) рішеннями:
- Комунікація між
системами симетрична: якщо система A може з'єднатись
з системою B, тлді система B мусить також бути в
змозі з'єднатись з системою A незалежним чином.
- Роль комунікаційних
систем симетрична в сенсі шаблонів з'єднання: немає
системи, що тільки сприймає з'єднання, та немає
системи, що тільки ініціює з'єднання.
Слідоцтвом
з ціх рішень є те, що неможливо безпечно створити
чисту клієнт-серверну установку зі здалегідь
визначеними ролями (порушення допущення 2). Для
клієнт-серверних установок краще використовувати HTTP
або Akka I/O.
Важливо:
Використання установок, що включають Network Address
Translation, Load Balancers або контейнери Docker
порушують припущення 1, якщо не були задіяні додаткові
кроки конфігурації мережі, що дозволяють симетричні
комунікації між причетними системами. В таких
ситуаціях Akka може бути сконфігуровано для прив'язки
до іншої мережевої адреси, інж та, що використовується
для встановлення з'вязку між вузлами Akka.
Дивіться Віддалена
конфігурація для NAT та Docker.
Точки
помітки для маштабування вгору за допомогою
маршрутизаторів
На
додаток до можливості виконувати різні частини системи
акторівна різних вузлах кластеру, також можливе
маштабуватись вгору на більше число ядер, примножуючи
суб-дерева акторів, що підтримує паралелізацію
(думайте, наприклад, про пошукоу машину, що підтримує
різні запити пошуку одночасно). Клони потім можуть
бути маршрутизовані різним чином, наприклад, по кругу.
Єдиною річчю, необхідною щоб досягти це, це розробник
має декларувати певного актора як “withRouter”,
потім — на його місці — буде створений
актор-маршрутизатор, що буде відроджувати
конфігуровану кількість дітей бажаного типу, та
маршрутизувати до них сконфігурованим чином. Тільки
такий маршрутизатор буде декларовано, його
конфігурація може бути вільно перекрати з файлу
конфігурації, включаючи змішування з віддаленим
розгортанням (деяких) дітей. Читайте більше щодо цього
в Маршрутизація
(Scala)
та Маршрутизація
(Java).
Akka
та
модель пам'яті Java
Головна
вигода від використання Typesafe Platform, включаючи
Scala та Akka, в тому, що це спощує процес написання
конкурентних програм. Ця стаття дискусує, як Typesafe
Platform, та зокрема Akka, підходить до розподіленої
пам'яті в конкурентних застосуваннях.
Модель
пам'яті Java
До
Java 5 модель пам'яті Java Memory Model (JMM) була
хворобливо визначена. Було можливим отримати всі
типи дивних результатів, коли розділена пам'ять
використовувалась з декількох потоків, як:
- потік не бачив
значення, записані іншими потоками: проблема
видимості
- потік бічив
'неможливу' поведінку інших потоків, що
викликалось виконанням інструкцій в порядку, що не
був очікуваний: проблема перестановки інструкцій.
З
реалізацією JSR 133 в Java 5 багато з ціх питань
були вирішені. JMM є набором правил, що базуються на
відношенні "відбуватись-раніше", що обмежує, коли
один доступ до пам'яті відбувається перед іншим, та
навпаки, коли їм дозволяється траплятись не у
порядку. Два приклада ціх правил:
- Правило
моніторингу блокування:
вивільнення блокування відбувається перед
кожним наступним захопленням того ж
блокування.
- Правило
непостійної змінної:
запис непостійної змінної відбувається перед
наступним читанням тієї самої непостійної
змінної
Хоча
JMM може виглядати заскладним, специфікація
намагається знайти баланс між простотою
використання, та можливістю писати продуктівні та
маштабуємі конкурентні структури даних.
Актори
та Java Memory Model
З
реалізацією акторів в Akka, є два шляхи, якими
потокі можуть виконувати акторів в розподіленій
пам'яті:
- якщо повідомлення
надсилається до актора (іншим актором). В
більшості випадків повідомлення незмінні, але якщо
це повідомлення не є вірно сконструйованим
незмінним об'єктом, без правила "відбувається
раніше" , може бути можливим для отримувача бачити
частково ініціалізовані структури даних, та,
можливо, навіть значення зі стелі повітря
(long/double).
- якщо актор робить
зміни до свого внутрішнього стану під час обробки
повідомлення, то отримує доступ до того стану під
час обробки іншого повідомлення декілька моментів
пізніше. Важливо уявляти, що з моделлю акторів ви
не отримуєте жодних гарантій, що той же потік буде
виконувати того ж актора для різних повідомлень.
Щоб
уникнути проблем видимості та перестановки для
акторів, Akka гарантує наступні два правила
"відбуватись раніше":
- Правило
надсилання актора:
надсилання повідомлення до актора відбувається
перед отриманням цього повідомлення тим же
актором.
- Правило
послідовної обробки актора:
обробка одного повідомлення відбувається перед
обробкою наступного повідомлення тим же
актором.
Зауваження
В
непрофесійних термінах це означає, що зміни до
внутрішніх полів актора видимі, коли наступне
повідомлення обробляється цім актором. Так що полі
вашого актора не мають бути непостійними або
еквівалентними.
Обоє
правил стосуються тільки того ж примірника актора,
та не діють, якщо використовуються різні актори.
Future
та Java Memory Model
Завершення
Future "відбувається перед" викликом любого
зворотнього виклику, зареєстрованого для виконання.
Ми
рекомендуємо не замикати на не-фінальні поля (final
в Java та val в Scala), та якщо ви замикаєте
на нефінальні поля, вони мають бути
зроблені volatile,
щоб поточне значення поля було дидиме в
зворотньому виклику.
Якщо
ви замикаєте на посиланні, ви повинні також
переконатись, що примірник, на який посилаються,
безпечний щодо потоків. Ми дуже рекомендуємо стояти
осторонь об'єктів, що використовують блокування,
оскільки це може ввести проблеми продуктивності, та,
в гіршому випадку, до тупих кутів. Це є небезпеками
синхронізації.
STM
та Java Memory Model
Akka's
Software Transactional Memory (STM) також провадить
правило "відбувається перед":
- Правило
транзакційного посилання:
вдалий запис під час закріплення, на посиланні
до транзакції, відбувається перед любим
наступним читанням того ж транзакційного
посилання.
Це
правило виглядає більше як правило 'непостійної
змінної' з JMM. Наразі Akka STM підтримує тільки
відкладений запис, так що дійсний запис до
розподіленої пам'яті відкладений до закріплення
транзакції. Запис впродовж транзакції покладається в
локальний буфер (набір записів транзакції), та не
видимий іншим транзакціям. Ось чому бруднічитання
неможливі.
Як
ці правила реалізовані в Akka є деталями реалізації,
та можуть змінюватись з часом, та конкретні деталі
можуть навіть залежати від використаної
конфігурації. Але вони будуть будуватись на інших
правилах JMM, як правило моніторингу
блокування, або правило непостійної змінної. Це
означає що ви, користувач Akka, не маєте хвилюватись
щодо додавання синхронізації, що впровадити такі
відносини "відбувається раніше", оскільки це
обов'язок Akka. Так що ви маєте руки вільними, щоб
вирішувати вашу базову логіку, та фреймворк Akka
запевняє, що ці правила гарантовано прислужаться
вам.
Актори
та розподілений змінний стан
Оскільки
Akka виконується в JVM, є ще деякі правила, що їм
треба слідувати.
- Замикання на
внутрішньому стані актора та демонстрація цього
іншим потокам
- class MyActor extends Actor {
- var state = ...
- def receive = {
- case _ =>
- // Невірно
-
- // Дуже погано, розподілений змінний стан
- // буде руйнувати ваше застосування збоченим шляхом
- Future { state = NewState }
- anotherActor ? message onSuccess { r => state = r }
-
- // Дуже погано, "sender" змінюється в кожному повідомленні,
- // баґ розподіленого змінного стану
- Future { expensiveCalculation(sender()) }
-
- // Правильнощі
-
- // Повністю безпечно, на "self" можна замикати
- // та це ActorRef, що безпечний до потоків
- Future { expensiveCalculation() } onComplete { f => self ! f.value.get }
-
- // Повністю безпечно, ми замикаємо на фіксованому значенні
- // та це ActorRef, що безпечний до потоків
- val currentSender = sender()
- Future { expensiveCalculation(currentSender) }
- }
- }
- Повідомлення повинні бути
незмінними, щоб уникнути пастки розподіленого
змінного стану.
Надійність
доставки повідомлень
Akka
допомагає вам будувати надійні застосування, що
використовують декілька процесорних ядер машини
(“маштабування догори”) або розподілятись по
комп'ютерній мережі (“маштабування вшир”). Ключовою
абстракцією, що змушує це робити, це те, що всі
взаємодії між частинами вашого коду — акторами —
відбувається через передачу повідомлень, ось чому
чтона семантика того, як повідомлення передаються
між акторами заслуговує на свою окрему главу.
Щоб
отримати деякий контекст для дискусії нижче, уявіть
застосування, що розташоване на декількох мережевих
вузлах. Базовий механізм для комунікації є той же,
чи посилається до актора на локальній JVM, або до
віддаленого актора, але, звичайно, будуть помітна
різниця в латентності доставки (можливо також
залежність від полоси пропускання мережевого
з'єднання, та розміру повідомлення), та надійність.
В випадку, коли надсилається віддалене повідомлення,
є очевидним, що задіяно більше кроків, що означає,
що більше речей може піти не так. Інший аспект
локального надсилання буде в простій передачі
посилання на повідомлення всередині тієї ж JVM, без
жодний обмежень до підлеглого об'єкту, що наісланий,
тоді як віддалений транспорт буде накладати ліміт на
розмір повідомлення.
Написання
ваших акторів так, що кожна інтеракція можливо буде
віддаленою, є безпечною, песімістичною ставкою. Це
означає, що треба покладатись тільки на ті
властивості, що завжди гарантовані, та які
дискутуються детально нижче. Це, звичайно, має деяке
навантаження в реалізації актора. Якщо ви бажаєте
поступитись повнії прозорості розміщення —
наприклад, в випадку групи тісно співпрацюючих
акторів — ви можете розміщувати іх завжди на тій
самій JVM, та насолоджуватись жосткішими гарантіями
доставки повідомлень. Деталі та компроміси пояснені
далі нижче.
В
якості додатка ми надаємо декілкьа вказівок, як
побудувату міцнішу надійність на основі
вбудованої. Ця глава закривається дискусією про роль
“Офісу померлих листів” (Dead Letter Office).
Головні
правила
Це
правила для надсилання повідомлень (тобто
методів tell або !,
що також відповідають шаблону ask):
- доставити-щонайбільше-раз,
тобто доставка не гарантована
- порядок
повідомлень для пари відсилач-отримувач
Перше
правило типово можна знайти в інших реалізаціях
акторів, тоді як друге специфічне для Akka.
Дискусія:
що означає “щонайбільше-раз”?
Коли доходить до опису семантики механізму доставки, є три базові категорії:
- щонайбільше-раз доставка означає, що для кожного повідомлення, переданого до механізму, це повідомлення буде доставлене нуль або один раз; в більш звичайних термінах це означає, що повідомлення може бути втрачене.
- щонайменьше-раз доставка означає, що для кожного повідомлення, переданого до механізму, потенційно робиться кілька спроб щодо його доставки, так що щонайменьше має бути успішним; знову, в більш звичайних термінах це означає, що може виникнути дублікація, але не втрата.
- точно-раз доставка означає, що для кожного повідомлення, переданого до механізму, відбувається рівно одна досавка до отримувача; повідомлення не може не бути втрачене, а ні продубліковано.
Перший випадок найдешевший — вища продуктивність,
менше навантаження реалізації — оскільки вона може бути виконана в стилі підпалив-і-забув, без зберігання стану на надсилаючій стороні або в механізмі передачі. Друге потребує повторів для підрахунку транспортних страт, що означає зберігання стану на стороні відсилки та мати механізм підтвердження на стороні підсилки.
Третє найбільш коштовне — та отже має найгіршу продуктивність — оскільки на додаток до другого він потребує, щоб стан зберігався і на боці отримувача, щоб фільтрувати дублікуючі доставки.
Дискусія:
чому немає гарантованої доставки?
В корені проблеми лежить питання, що саме повинне означати гарантовано:
- Повідомлення було надіслане з мережі?
- Повідомлення передане іншим вузлом?
- Повідомлення покладене в поштову скриньку цільового актора?
- Повідомлення почало оброблятись цільловим актором?
- Повідомлення умпішно оброблене цільовим актором?
Кожне з перерахованого має різні виклики та вартість, та є очевиним, що є умови, коли люба бібліотека передачі повідомленнь не буде в змозі задовільмити; уявіть, наприклад, конфігуровані типи поштові скриньки, та як обмежена поштова скринька буде взаємодіяти з третім пунктом, або навіть що це означає визначитись з частиною “успішно” п'ятого пункта.
За цім слідують мірківання щодо того, що Ніхто не потребує надійної доставки. Єдиним осмисленим шляхом для надсилача знати, чи взаємодія була успішною, є отримання повідомлення підтвердження бізнес рівня, що не є тим, що Akka може зробити сама по собі
(ми ані пишемо фреймворк “роби що я маю на увазі”, ані ви не бажаєте, щоб ми це робили).
Akka обіймає розподілене обчислювання, та робить схильність до падінь комунікацій явною через передачу повідомлень, і таким чином не намагається брехати та емулювати діряву абстракцію. Це модель, що з великим успіхом використовувалась в Erlang, та потребує від користувачів розробляти свої застосування коло неї. Ви можете прочитати більше щодо цього підходу в документації Erlang (розділи 10.9 та 10.10), Akka близько слідує йому.
Інший аспект цієї проблеми в тому, що провадження тільки базових гарантій тих випадків використання, що не потребують сильнішої надійності, що не додають вартості до їх реалізації; є завжди можливим додати сильнішу надійність зверху базових,
але не можливо заднім числом видалити надійність, щоб отримати більшу продуктивність.
Дискусія:
впорядкування повідомлень
Правило, більш конкретно, полягає в тому, що для даної пари акторів, повідомлення, надіслані напряму від першого до другого, не будуть отримані невпорядковані. Слово напряму наголошує, що ця гарантія стосується тільки коли надсилається за допомогою telloperator
до кінцевого пункту призначення, але не тоді, коли задіються медіатори, або інші можливості поширення повідомлень (коли не вказане інше).
Гарантія ілюструється наступним чином:
Актор A1 надсилає повідомлення M1, M2, M3 до A2
Актор A3 надсилає повідомлення M4, M5, M6 до A2
- Це означає наступне:
-
- Якщо
M1 доставлене, воно має бути доставлене до M2 та M3
- Якщо
M2 доставлене, воно має бути доставлене до M3
- Якщо
M4 доставлене, воно має бути доставлене до M5 та M6
- Якщо
M5 доставлене, воно має бути доставлене до M6
A2 може бачити повідомлення від A1 перемішані з повідомленнями від A3
- Оскільки немає гарантії доставки, кожне з повідомлень може бути загублене, тобто не надійти до
A2
Зауваження
Важливо зауважити, що гарантія Akka
стосується до порядку, в якому повідомлення ставляться в чергу до поштової скриньки реціпієнта. Якщо реалізація поштової скриньки не притрумується порядку FIFO (тобто, PriorityMailbox),
тоді порядок обробки актором може відхилятись від порядку постановки в чергу.
Будь ласка, зауважте, що це павило не транзитивне:
Актор A надсилає повідомлення M1 актору C
Актор A потім надсилає повідомлення M2 актору B
Актор B пересилає повідомлення M2 актору C
Актор C може отримати M1 та M2 в довільному порядку
Принагідне транзитивне впорядкування буде мати на увазі, щоM2 ніколи не буде отримане перед M1 atна акторіC (хоча кожне з них може бути втрачене). Цей порядок може бути порушений через різні затримки доставлення повідомлень, колиA, B та C знаходяться на різних вузлах мережі, більше дивіться нижче.
Зауваження
Створення актора трактується як повідомлення, надіслане від батька до дитини, з тою ж семантикою, що обгорювалась вище. Надсилання повідомлення до актора в спосіб, що може бути перевпорядкований цім початковим повідомленням створення означає, що повідомлення може не з'явитись, оскільки актор ще не існує. Приклад, коли повідомлення може з'яаитись дуже рано, може бути спроба створити віддалено розташованого актора R1, надсилання його посилання до іншого віддаленого актора R2, та цей R2 надсилає повідомлення до R1. Приклад гарно визначеного впордкування є батько, який створює актора, та безпосередньо надсилає йому повідомлення.
Комунікація відмови
Будь ласка зауважте, що гарантії порядку, дискутовані вище, дотримуються для користувацьких повідомлень між акторами. Відмова дитини актора комунікується через спеціальну систему повідомлень, що не впорядковані відповідно до звичайних користувацьких повідомлень. Зокрема:
Актор-дитина C надсилає повідомлення M батьку P
Актор-дитина дає збій F
Батько P може отримати два повідомлення в любому порядку M, F або F, M
Причина полягає в тому, що внутрішня система повідомлень має свої власні поштові скриньки, так що порядок викликів черги користувацьких та системних повідомлень не може гарантувати порядок часу їх отримання з черги.
Правила для повідомлень локальної JVM
Будьте уважні щодо того, що робити з цім розділом!
Покладання на сильнішу надійність цього розділу не рекомендована, оскільки це може прив'язати ваше застосування до тільки локального розгортання: застосування може бути розроблене інакше
(на відміну від застосування тільки деяких шаблонів обміну повідомленнями, локальних для деяких акторів), щоб виконуватись на кластері машин. Наше кредо є “розробляємо раз, розгортаємо де побажаєте”, та щоб досягти цього вам треба покладатись тільки на Загальні правила.
Надійність посилок на локальній машині
Тестова сюїта Akka покладається на те, що в локальному контексті повідомлення не губляться (та для тестів на умову відсутності помилок і для віддаленого розгортання), що значить, що ми насправді докладаємо найкращих зусиль, щоб утримуват наші тести стабільними. Однак локальна операція tell може схибити з деякої причини, так само, як звичайний виклик метода на JVM:
StackOverflowError
OutOfMemoryError
- інша
VirtualMachineError
На додаток локальні посилки можуть схибити у специфічний для Akka спосіб:
- якщо поштова скринька не примає повідомлення (тобто повний
BoundedMailbox)
- якщо отримуючий актор схибить при обробці повідомлення, або вже завершений
Коли перше є очевидним наслідком конфігурації, друге є приводом замислитись: надсилач повідомлення не отримує зворотнього зв'язку, якщо під час обробки виникло виключення, замість цього це повідомлення іде до супервізора. Загалом, для зовнішнього спостерігача, це неможливо відрізнити від втрати повідомлення.
Порядок надсилань локальних повідомлень
Якщо припустити поштові скриньки з суворим FIFO, вже зазначене застереження щодо відсутності гарантії транзитивності порядку повідомлень відсутнє за деяких умов. Як ви можете зазначити, це самі по собі дуже тонкі матерії, та навіть можливо, що подальші оптимізації продуктивності зроблять недійсним весь цей параграф. Можливо, неповний перелік контр-показань наступний:
- Перед отриманням першої відповіді від актора верхнього рівня, існує блокування, що захищає внутрішню проміжну чергу, та цей блок не є чесним; наслідок полягає в тому, що запити в черзі від різних надсилачів, що надходять на протязі конструювання актора (фігурально, деталі більш складні), можуть бути переставлені, в залежності від низькорівневого планування потоків.
Оскільки на JVM не існують повністю чесні блокування, це неможливо полагодити.
- Той же механізм використовується на протязі конструювання the construction of a
Router-а, біль точно маршрутизованого ActorRef,
оскільки та ж проблема існує для акторів, розгорнутих за допомогою Routers.
- Як зазначено вище, проблема виникає скрізь, де задіяне блокування в процесі постаки в чергу, що також може стосуватись власних поштових скриньок.
Цей список був уважно підібраний, але інші проблематичні сценарії, можливо, уникли нашого аналізу.
Як локальне впорядкування співвідноситься з мережевим впорядкуванням
Правило полягає в тому, що, для окремої пари акторів, повідомлення, надіслані безпосередньо від першого до другого, не будуть отримані з порушенням порядку, якщо повідомлення надсилаються через мережу за допомогою базованого на TCP віддаленого транспортного протоколу Akka.
Як пояснено в попередньому розділі, надіслані локальні повідомлення підкорюються випадковому транзитивному порядку за певних умов. Цей порядок може бути порушений через різні затримки доставки повідомлень. Наприклад:
Актор A на
node-1 надсилає повідомлення M1 актору C на
node-3
Актор A на
node-1 потім надсилає повідомлення M2 актору B на
node-2
Актор B на
node-2 пересилає повідомлення M2 актору C на
node-3
Актор C може отримати M1 та M2 в довільному порядку
Для M1 може знадобитись більше часу для
"мандрівки" до node-3, ніж забирає "мандрівка" M2 до node-3 через node-2.
Абстракції вищого порядку
Базуючись на малому та послідовному наборі інструментів,
Akka також провадить потужні, високорівневі абстракції зверху них.
Шаблони обміну повдіомленнями
Як дискутувалось вище, прямолінійна відповідь на запит надійної доставки є явний протокол ACK–RETRY. В простішій формі це потребує наступне
- спосіб ідентифікувати окремі повідомлення, щоб пов'язати повідомлення з підтвердженням
- механіз повторів, що буде знову надсилати повідомлення, якщо підтвердження не прийшло вчасно
- спосіб для отримувача визначити та відкинути дублікати
Третє стає потрібним завдяки тому, що доставк підтвердження також не гарантована. Протокол ACK-RETRY з підтвердженнями бізнес-рівня підтримується Щонайменьше-раз доставкою модуля Akka Persistence. Дублікати можуть бути виявлені через відсліджування ідентифікаторів повідомлень, відісланих через Щонайменьши-раз дооставку. Інший шлях реалізувати третю частину - це зробити обробку повідомлень незалежною на рівні бізнес логіки.
Інший приклад реалізації всіх трьох вимог відомий як Шаблон надійного проксі (що зараз замінений Щонайменьше-раз доставкою).
Підбір подій
Підбір подій (та шардінг) - це те, що дозволяє великим веб сайтам зростати до мільярдів користувачів, хоча ідея є досить простою: коли компонент (в нашому випадку актор) обробляє команду, він генерує список подій, що представляють ефект команди. Ці події на додаток застосовуються до стану актора. Гарна річ щодо цієї схеми в тому, що події тільки додаються до сховища, ніщо ніколи на змінюється; це дозволяє чудову реплікацію та маштабування споживачів цього потоку подій (тобто інші компоненти можуть споживати потік подій як спосіб реплікації стану компонента на іншому континенті, або як реакція на зміни). Якщо стан компонента втрачено — через відмову машини або тому що він виштовхнутий з кеша — він може бути легко реконструйований програванням потоку подій (звичайно задіявши знімок, щоб прискорити процес). Підбір подій підтримується Akka Persistence.
Поштова скринька з явним підтвердженням
Реалізуючи власний тип поштової скриньки, можливо повторити обробку повідомлення з боку отримуючого актора, щоб обробити тимчасові збої. Цей шаблон найбільш корисний в локальному контексті комунікації, коли гарантії доставки і так достатні для потреб застосування.
Будь ласка зазначте, що застосовуються особливості для Правил локального надсилання JVM.
Приклад реалізації цього шаблона показаний в Поштовій скриньці з явним підтвердженням.
Мертві листи
Повідомлення, що не можуть бути доставлені (та для яких це можна стверджувати) будуть доставлені до синтетичного актора на ім'я/deadLetters.
Ця доставка відбувається на основі кращого-зусилля; при цьому збій може відбутись навіть на локальній JVM (наприклад під час завершення актора). Повідомлення, надіслані через ненадійі мережеві транспорти будуть губитись, не перетворюючись на мертві листи.
Для чого я можу використовувати мертві листи?
Головне використання цієї можливості є налаштування,
особливо якщо посилки акторана з'являються узгоджено (тоді, можливо, перегляд мертвих листів підкаже вам, що надсилач або отримувач був десь налаштований неврно). Щоб бути корисними в цей спосіб, є гарною практикою уникати надсилання deadLetters, якщо це можливо, тобто час від часу виконувати застосування з підходящим логером мертвих листів (діивіться нижче), та очищати вивід логера. Ця вправа — я все інше — потребує розважливого застосування здорового грузду: дуже може статись, що запобігання надсилання завершеному актору ускладнює код надсилача, більше, ніж отримується через ясність виводу налаштування.
Сервіс мертвих листів слідує тим же правилам, з повагою до гарантій доставки, що і надсилання інших повідомлень, так що це не може використовуватись для гарантованої доставки.
Як мені отримувати мертві листи?
Актор може підписатись на клас akka.actor.DeadLetter на потоці подій, дивіться Потік подій (Java) або Потік подій (Scala), щоб подивитись як це робиться. Підписаний актор буде отримувати всі мертві листи, опубліковані в
(локальній) системі з цього часу і надалі. Мертві листи не передаються по мережі, якщо ви бажаєте збирати їх в одному місці, вам треба підписати по одному актору на кожний вузол, та потім переправляти їх вручну. Також майте не увазі, що мертві листи генеруються на тому вузлі, який може визначити, що операція надсилання схибила, що для віддаленого надсилання може бути локальна система (якщо мережеве з'єднання не встановлене), або віддалений вузол (якщо актор, до якого іде повідомлення, не існує в цей проміжок часу).
Мертві листи, що (звичано) не викликають турботи
Кожного разу, коли актор не завершується за власним рішенням, є шанс, що деякі повідомлення, що він надсилає собі, будуть втрачені. Є дещо, що трапляється досить просто в складних сценаріях завершення, що часто відбуваються: надсилання повідомлення akka.dispatch.Terminate відкидається, що означає, що були надані два запити на завершення, але, звичайно, тільки один був успішним. В тому ж дусі ви можете бачити повідомленняakka.actor.Terminated
від дітей при зупинці ієрархії акторів, що перетворюються на мертві листи, якщо батько все ще наглядає за дітьми, коли сам батько завершується.
Конфігурація
Ви можете почати використовувати Akka без визначення жодної конфігурації, оскільки провадяться осмислені значення по замовчанню. Потім вам може стати потрібно змінити налаштування, щоб змінити поведінку по замовчанню, або адаптуватись під специфічні середовища виконання. Типовимі приклади налаштувань, які ви можете змінити:
- рівень журналу та сам логер
- ввімкнути віддалене розгортання
- серіалайзери повідомлень
- визначення маршрутизаторів
- підгонка диспечерів
Akka
використовує бібліотеку Typesafe
Config Library, що також може бути гарним вибором для конфігурації вашого власного застосування або бібліотеки, створених за допомогою, або без, Akka. Ця бібліотека реалізована в Java без зовнішніх залежностей; вам слідує переглянути її документацію (зокрема щодо ConfigFactory),
що тільки підсумовується далі.
Попередження
Якщо ви використовуєте Akka в Scala REPL від серії
2.9.x, та ви не провадите ваш власний ClassLoader для ActorSystem, запустіть REPL з "-Yrepl-sync", щоб обійти недолік в наданому REPL Context ClassLoader.
Звідки читається конфігурація
Вся конфігурацію для Akka утримується в примірниках ActorSystem,
або, кажучи іншими словами, як це бачиться ззовні,ActorSystem є єдиним споживачем інформації конфігурації. Коли створюється система акторів ви можете або передати їй об'єктConfig, або ні, при чому другий випадок еквівалентний передачі ConfigFactory.load() (з потрібним завантажувачем класа). Грубо кажучи це означає, що по замовчанню розбираються всі application.conf, application.json та application.properties, знайдені в корені шляху classpath — будь ласка, звертайтесь до вищезгаданої документації щодо деталей.
Система акторів потім поєднує всі ресурси з reference.conf, знайдених в корені classpath, щоб зформувати конфігурацію для відкату, тобто внутрішньо використовує
- appConfig.withFallback(ConfigFactory.defaultReference(classLoader))
Філософія полягає в тому, що код ніколи не містить значення по замовчанню, але замість цього покладається на їх присутність в reference.conf, що надається разом з розгляданою бібліотекою.
Найвищий приоритет надається значенням, що перекривають надані системні властивості, дивіться специфікацію
HOCON (ближче до кінця). Також варто зауважити, що конфігурація застосування — що по замовчанню application — може бути переписана з використаннямвластивостіconfig.resource (щодо деталей будь ласка посилайтесь до документації з конфігурації).
Зауваження
Якщо ви пишете застосування Akka, тримайте свою конфігурацію в application.conf в корені classpath. Якщо ви пишете бібліотеку на основі Akka, тримайте конфігурацію в reference.conf в корені JAR файла.
Випадок використання JarJar, OneJar, Assembly або іншого jar-бандлера
Попередження
Підхід до конфігурації Akka значно покладається на нотацію, коли модуль/jar має свій власний файл reference.conf, все це може бути виявлене через конфігурацію та завантажено. На жаль
це також означає, що якщо ви складаєте/об'єднуєте декілька jar в такий самий jar,
вам треба також злити всі reference.confs. Інакше всі замовчання будуть втрачені, та Akka не буде функціонувати.
Якщо ви використовуєте Maven для пакування вашого застосування, ви також можете використовувати підтримку Apache
Maven Shade Plugin для Resource
Transformers, щоб об'єднувати всі reference.confs по classpath побудови в один.
Конфігурація плагіна може виглядати так:
- <plugin>
- <groupId>org.apache.maven.pluginsgroupId>
- <artifactId>maven-shade-pluginartifactId>
- <version>1.5version>
- <executions>
- <execution>
- <phase>packagephase>
- <goals>
- <goal>shadegoal>
- goals>
- <configuration>
- <shadedArtifactAttached>trueshadedArtifactAttached>
- <shadedClassifierName>allinoneshadedClassifierName>
- <artifactSet>
- <includes>
- <include>*:*include>
- includes>
- artifactSet>
- <transformers>
- <transformer
- implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
- <resource>reference.confresource>
- transformer>
- <transformer
- implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
- <manifestEntries>
- <Main-Class>akka.MainMain-Class>
- manifestEntries>
- transformer>
- transformers>
- configuration>
- execution>
- executions>
- plugin>
Власний application.conf
Власний application.conf може виглядати так:
- # В цьому файлі ви можете перекрити опції, визначені в файлах посилання.
- # Копіюйте частинами з файлів посилань та змінюйте за бажанням.
-
- akka {
-
- # Логери для реєстрації під час завантаження
- # (akka.event.Logging$DefaultLogger пише до STDOUT)
- loggers = ["akka.event.slf4j.Slf4jLogger"]
-
- # Рівень журналу для використання сконфігурованим логером (див "логери")
- # як тільки пони стартують; до того дивіться "stdout-loglevel"
- # Опції: OFF, ERROR, WARNING, INFO, DEBUG
- loglevel = "DEBUG"
-
- # Рівень журналу для дуже базового логера, активованого впрдовж запуску
- # ActorSystem. Цей логер друкує повідомлення в stdout (System.out).
- # Опції: OFF, ERROR, WARNING, INFO, DEBUG
- stdout-loglevel = "DEBUG"
-
- # Фільтр подій журналу, що використовується LoggingAdapter перед
- # публікацією в журнал подій до eventStream.
- logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
-
- actor {
- provider = "akka.cluster.ClusterActorRefProvider"
-
- default-dispatcher {
- # Перебіг через Dispatcher по замовчанню, 1 для максимально чесного
- throughput = 10
- }
- }
-
- remote {
- # Порт, до якого під'єнуються клієнти. По замовчанню 2552.
- netty.tcp.port = 4711
- }
- }
Включення файлів
Іноді може бути корисним включити інший файл конфігурації, наприклад, якщо ви маєте одинapplication.conf з усіма незалежними від оточення налаштуваннями, та потім перекриваєте деякі налаштування для особливих оточень.
Вказавши системну властивість за допомогою-Dconfig.resource=/dev.conf завантажить файл dev.conf,
що включатиме application.conf
dev.conf:
- include "application"
-
- akka {
- loglevel = "DEBUG"
- }
Більш складне включення та механізми заміни пояснені в специфікації HOCON.
Журналювання конфігурації
Якщо системна властивість або конфігурація akka.log-config-on-start встановлена в on,
тоді при старті системи акторів повна конфігурація скидається в журнал на рівні
INFO.
Це корисно, коли ви не впевнені щодо того, яка конфігурація використовується.
Якщо маєте сумніви, ви також можете просто і мило перевірити об'єкти конфігурації перед або після їх використання для конструювання системи акторів:
- Welcome to Scala version 2.11.7 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0).
- Type in expressions to have them evaluated.
- Type :help for more information.
-
- scala> import com.typesafe.config._
- import com.typesafe.config._
-
- scala> ConfigFactory.parseString("a.b=12")
- res0: com.typesafe.config.Config = Config(SimpleConfigObject({"a" : {"b" : 12}}))
-
- scala> res0.root.render
- res1: java.lang.String =
- {
- # String: 1
- "a" : {
- # String: 1
- "b" : 12
- }
- }
Коментарі, що передують кожному елементу, надають детальну інформацію щодо походження налаштувань
(файл та номер рядка), плюс можливі присутні коментарі, тобто в відповідній конфігурації. Налаштування, що поєднані з посиланням та розібрані системою акторів можуть відображуватись таким чином:
- final ActorSystem system = ActorSystem.create();
- System.out.println(system.settings());
- // це скорочення до system.settings().config().root().render()
Слово щодо ClassLoaders
В декількох місцях файла конфігурації можливо вказати повністю кваліфіковане ім'я класа, що деінде створюється Akka.
Це робиться через рефлексію Java, що, в свою чергу, використовує ClassLoader.
Отримання потрібного в складному середовищі, як контейнери застосувань або
OSGi пакунки не є завжди тривіальним, нагальний підхід Akka в тому, що кожна реалізація ActorSystem зберігає поточний завантажувач класів в контексті потоку (якщо він доступний, інакше тільки власний завентажувач, як в this.getClass.getClassLoader), та використовує його для всіх доступів через рефлексію.
Це означає, що ставлячи Akka на classpath завантаження буде видаватиNullPointerException в дивних місціх: це просто не підтримується.
Специфічні до застосування налаштування
Конфігурація також може використовуватись для специфічних до застосування налаштувань. Гарною практикою є розміщення ціх налаштувань в Extension, як описано нижче:
Конфігурацію декількох ActorSystem
Якщо ви маєте більше ніж однуActorSystem (або ви пишете бібліотеку та маєте ActorSystem, що може бути відокремленою від системи акторів застосування), ви можете побажати відділити конфігурації для кожної з систем.
Приймаючи до уваги, що ConfigFactory.load() поєднує всі ресурси зі співпадаючими іменами по всьому шляху класів, найпростіше задіяти цю функціональність, та розрізнити системи акторів в ієрархії конфігурацій:
- myapp1 {
- akka.loglevel = "WARNING"
- my.own.setting = 43
- }
- myapp2 {
- akka.loglevel = "ERROR"
- app2.setting = "appname"
- }
- my.own.setting = 42
- my.other.setting = "hello"
- val config = ConfigFactory.load()
- val app1 = ActorSystem("MyApp1", config.getConfig("myapp1").withFallback(config))
- val app2 = ActorSystem("MyApp2",
- config.getConfig("myapp2").withOnlyPath("akka").withFallback(config))
Ці два приклади демонструють різні варіації прийому “задій-піддерево”: в першому випадку конфігурація, доступна з системи акторів, подібна до такої
- akka.loglevel = "WARNING"
- my.own.setting = 43
- my.other.setting = "hello"
- // поєднання піддерев myapp1 та myapp2
тоді як в другому випадку задіяне тільки піддерево “akka” з наступним результатом
- akka.loglevel = "ERROR"
- my.own.setting = 42
- my.other.setting = "hello"
- // поєднання піддерев myapp1 та myapp2
Зауваження
Бібліотека конфігурації насправді потужна,
пояслення всіх можливостей перевершує відведений тут простір. Зокрема, ми не охопили як включати файли конфігурації один в інший (дивіться простий приклад у Включенні файлв), та копіюват ичастини дерева конфігурації шляхом подміни шляху.
Ви також можете вказати та розібрати конфігурацію програмно, в інший спосіб, ніж створювати примірник ActorSystem.
- import akka.actor.ActorSystem
- import com.typesafe.config.ConfigFactory
- val customConf = ConfigFactory.parseString("""
- akka.actor.deployment {
- /my-service {
- router = round-robin-pool
- nr-of-instances = 3
- }
- }
- """)
- // ConfigFactory.load закладає customConfig між референсною конфігурацією
- // по замовчанню та перекриттями, то потім розрішує їх.
- val system = ActorSystem("MySystem", ConfigFactory.load(customConf))
Читання конфігурації з власного розташування
Ви можете замінити або доповнити application.conf або в коді, або використовуючи системні властивості.
Якщо ви використовуєте ConfigFactory.load() (що Akka робить по замовчанню), ви можете замінити application.conf, визначивши -Dconfig.resource=whatever, -Dconfig.file=whatever, або -Dconfig.url=whatever.
З середини вашого заміщаючого файла, вказаного через-Dconfig.resource та його товаришів, в можете включити include"application", якщо ви все ще бажаєте також використовуватиapplication.{conf,json,properties}. Налаштування, вказані доinclude "application" можуть бути переписані файлом включення, хоча після цього вони можуть перекрити включений файл.
В коді є багато опцій для налаштування.
Є декілька перевантаженьConfigFactory.load();
вони дозволяють вам вкуазувати дещо закладене між системними властивостями (що перекриваються), та замовчаннями (з reference.conf),
заміщуючи звичайне application.{conf,json,properties} за замінюючи -Dconfig.file з друзями.
Простіший варіантConfigFactory.load() приймає ресурс (замість application);myname.conf, myname.jsonта myname.properties після цього можуть бути задіяні замістьapplication.{conf,json,properties}.
Найбільш гнучкий варіант приймає об'єктConfig,
що може бути завантажений з використанням любого методуConfigFactory.
Наприклад, ви можете покласти рядок налаштування в код, використовуючи ConfigFactory.parseString(), або ви можете створити мапу, та ConfigFactory.parseMap(), або ви можете завантажити файл.
Ви також можете скомбінувати вашу власну конфігурацію зі звичайною, що може виглядати приблизно так:
- // зробити Config з тільки вашими особистими налаштуваннями
- Config myConfig =
- ConfigFactory.parseString("something=somethingElse");
- // завантажити звичайний стек конфігурації (системні властивості,
- // потім application.conf, потім reference.conf)
- Config regularConfig =
- ConfigFactory.load();
- // перекриваємо звичайний стек нашим myConfig
- Config combined =
- myConfig.withFallback(regularConfig);
- // покласти результат посередині між перекриттями
- // (системними властивостями) та замовченнями
- Config complete =
- ConfigFactory.load(combined);
- // створюємо ActorSystem
- ActorSystem system =
- ActorSystem.create("myname", complete);
При роботі з об'єктами Config,
тримайте на увазі, що в цьому торті є три "прошарка":
ConfigFactory.defaultOverrides() (системні властивості)
- налаштування застосування
ConfigFactory.defaultReference() (файл reference.conf)
Звичайною ціллю є налаштування проміжного слою, тоді як інші два залишаються як є.
ConfigFactory.load() завантажує повний стек
- перекриття з
ConfigFactory.load() дозволяє вам вказати відмінний проміжний прошарок
- варіації
ConfigFactory.parse() завантажують окремі файли та ресурси
Щоб накласти два прошарка використовуйтеoverride.withFallback(fallback);
спробуйте утримувати системні властивості (defaultOverrides())
зверху, та reference.conf (defaultReference()) знизу.
Майте на увазі, що часто ви можете тільки додати твердження include до application.conf, скоріше, ніж писати код. Вставлення на початку application.conf будуть перекриті залишком application.conf,
тоді як вставки наприкінці будуть перекривати ранішні налаштування.
Конфігурація розгортання актора
Налаштування розгортання для окремих акторів може бути визначена в розділі конфігураціїakka.actor.deployment. В розділі розгортання можливо визначити такі речі, як диспечер, поштова скринька, налаштування маршрутизатора та віддалене розгортання. Конфігурація ціх можливостей описана в главах, де розкриті відповідні теми. Приклад може виглядати так:
- akka.actor.deployment {
-
- # '/user/actorA/actorB' є віддалено розміщеним актором
- /actorA/actorB {
- remote = "akka.tcp://sampleActorSystem@127.0.0.1:2553"
- }
-
- # всі прямі діти '/user/actorC' мають виділений диспечер
- "/actorC/*" {
- dispatcher = my-dispatcher
- }
-
- # '/user/actorD/actorE' має окремий приоритет поштової скриньки
- /actorD/actorE {
- mailbox = prio-mailbox
- }
-
- # '/user/actorF/actorG/actorH' є випадковим пулом
- /actorF/actorG/actorH {
- router = random-pool
- nr-of-instances = 5
- }
- }
-
- my-dispatcher {
- fork-join-executor.parallelism-min = 10
- fork-join-executor.parallelism-max = 10
- }
- prio-mailbox {
- mailbox-type = "a.b.MyPrioMailbox"
- }
Зауваження
Розділ розгорнення для окремого актора ідентифікується шляхом актора відповідно до /user.
Ви можете використовувати зірочки для розділів шляху актора, так що ви можете вказати: /*/sampleActor, та це може зпівпадати з усімаsampleActor на цьому рівні ієрархії. Ви також можете використовувати зірочки в останній позиції, для співпадіння з усіма акторами на певному рівні: /someParent/*.
Не-узагальнені співпадіння завжди мають вищий приоритет, ніж співпадіння з зірочками, так що/foo/bar буде визнаний більш специфічним, ніж /foo/*, та буде використаний тільки більш вищий приоритет співпадіння.
будь ласка, зауважте, що це не може бути використане для розділу часткового співпадіня, як це: /foo*/bar, /f*o/bar та подібне до цього.
Перегляд референсної конфігурації
Кожний модуль Akka має файл референсної конфігурації зі значеннями за замовченням.
akka-actor
- ####################################
- # Akka Actor Reference Config File #
- ####################################
-
- # Це референсний файл конфігурації, що містить всі налаштування по замвчанню.
- # Робіть ваші редакції/перекриття в вашому application.conf.
-
- akka {
- # Версія Akka, відповідно до робочої версії Akka.
- version = "2.4.1"
-
- # Домашній каталог Akka, модулі в цьому каталозі будуть завантажені
- home = ""
-
- # Логери, що реєструються під час завантаження
- # (akka.event.Logging$DefaultLogger пише до STDOUT)
- loggers = ["akka.event.Logging$DefaultLogger"]
-
- # Фільтр подій журналу, що використовується LoggingAdapter перед
- # публікацією подій журналу до eventStream. Він може виконувати
- # добре виважене фільтрування на основі витоку повідомлень. По замовчанню
- # реалізація фільтрує на основі `loglevel`.
- # FQCN LoggingFilter. Class FQCN мусить реалізувати
- # akka.event.LoggingFilter та мати публічний конструктор з параметрами
- # (akka.actor.ActorSystem.Settings, akka.event.EventStream).
- logging-filter = "akka.event.DefaultLoggingFilter"
-
- # Логери, створювані та зареєстровані синхронно під час запуску ActorSystem,
- # та оскільки це актори, цей таймаут використовується для обмеження
- # часу очікування
- logger-startup-timeout = 5s
-
- # Рівень журналу, що використовується для сконфігурованих логерів
- # як тільки вони стартують; перед цім дивіться "stdout-loglevel"
- # Опції: OFF, ERROR, WARNING, INFO, DEBUG
- loglevel = "INFO"
-
- # Рівень журналу для дуже базового логера, активованого під час запуску
- # ActorSystem. Цей логер друкує повідомлення журналу до stdout (System.out).
- # Опції: OFF, ERROR, WARNING, INFO, DEBUG
- stdout-loglevel = "WARNING"
-
- # Журналює повну конфігурацію на рівні INFO, коли стартує система акторів.
- # Це корисне, коли ви не впевнені, яка конфігурація використовується.
- log-config-on-start = off
-
- # Журналює на рівні info, коли повідомлення надсилаються до мертвих листів.
- # Можливі значення:
- # on: журналюються всі мертві повідомлення
- # off: мертві повідомлення не журналюються
- # n: позитивне ціле, число мертвих листів, що будуть журналюватись
- log-dead-letters = 10
-
- # Можливість відключити журналювання мертвих листів під час завершення
- # системи акторів. Журналювання виконується тільки коли включене
- # налаштування 'log-dead-letters'.
- log-dead-letters-during-shutdown = on
-
- # Список FQCN розширень, що будуть завантажені при запуску системи акторів.
- # Має бути в форматі: 'extensions = ["foo", "bar"]' тощо.
- # Дивіться документацію Akka щодо додаткових даних про розширення
- extensions = []
-
- # Перемикає, чи потоки, створені цією ActorSystem будуть демонами
- daemonic = off
-
- # Завершення JVM, System.exit(-1) в випадку фатальної помилки,
- # такої, як OutOfMemoryError
- jvm-exit-on-fatal-error = on
-
- actor {
-
- # FQCN до задіяного ActorRefProvider; нижче наведене значення по замовчанню,
- # інше значення akka.remote.RemoteActorRefProvider з пакунку akka-remote.
- provider = "akka.actor.LocalActorRefProvider"
-
- # Охоронець "/user" буде використовувати цей клас для отримання supervisorStrategy.
- # Це має бути субклас akka.actor.SupervisorStrategyConfigurator.
- # На додаток до замовчання є akka.actor.StoppingSupervisorStrategy.
- guardian-supervisor-strategy = "akka.actor.DefaultSupervisorStrategy"
-
- # Таймаут для ActorSystem.actorOf
- creation-timeout = 20s
-
- # Серіалізотори та десеріалізатори (не примітивні) повідомлень,
- # щоб впевнитись щодо незміності. Призначене тільки для тестування.
- serialize-messages = off
-
- # Створювання серіалізаторів та десеріалізаторів (в Props), щоб переконатись,
- # що вони можуть бути передані по мережі. Це призначено тільки для тестування.
- # Чисто локальні розгортання, помічені як deploy.scope == LocalScope
- # виключаються з перевірки.
- serialize-creators = off
-
- # Таймаут для операцій надсилання до акторів вищого рівня, що є в процесі
- # стартування. Це має відношення тільки в випадку, якщо використовується
- # прив'язана поштова скринька або theCallingThreadDispatcher
- # для акторів вищого рівня.
- unstarted-push-timeout = 10s
-
- typed {
- # Таймаут по замовчанню для типованих методів актора з непустим
- # типом результата
- timeout = 5s
- }
-
- # Відзеркалення коротких імен ´deployment.router' на повністю
- # кваліфіковані імена класів
- router.type-mapping {
- from-code = "akka.routing.NoRouter"
- round-robin-pool = "akka.routing.RoundRobinPool"
- round-robin-group = "akka.routing.RoundRobinGroup"
- random-pool = "akka.routing.RandomPool"
- random-group = "akka.routing.RandomGroup"
- balancing-pool = "akka.routing.BalancingPool"
- smallest-mailbox-pool = "akka.routing.SmallestMailboxPool"
- broadcast-pool = "akka.routing.BroadcastPool"
- broadcast-group = "akka.routing.BroadcastGroup"
- scatter-gather-pool = "akka.routing.ScatterGatherFirstCompletedPool"
- scatter-gather-group = "akka.routing.ScatterGatherFirstCompletedGroup"
- tail-chopping-pool = "akka.routing.TailChoppingPool"
- tail-chopping-group = "akka.routing.TailChoppingGroup"
- consistent-hashing-pool = "akka.routing.ConsistentHashingPool"
- consistent-hashing-group = "akka.routing.ConsistentHashingGroup"
- }
-
- deployment {
-
- # id шаблону розгортання в форматі /parent/child etc.
- default {
-
- # Це id диспечера, що буде використовуватись з цім актором.
- # Якщо воно не визначене або пусте, диспечер, використовується вказаний
- # в коді (Props.withDispatcher), або default-dispatcher, якщо
- # взагалі не вказане.
- dispatcher = ""
-
- # Це id поштової скриньки, що буде використовуватись з цім актором.
- # Якщо воно не визначене або пусте, поштова буде використана скринька
- # по замовчанню сконфігурованого диспечера, або, якщо поштова скринька не
- # задана в конфігурації, буде використана поштова скринька в коді
- # (Props.withMailbox). Якщо є скринька, визначена в сконфігурованому
- # диспечері, тоді вона перекриватиме це налаштування.
- mailbox = ""
-
- # схема маршрутизації (наланс навантаження), що буде задіяна
- # - доступні: "from-code", "round-robin", "random", "smallest-mailbox",
- # "scatter-gather", "broadcast"
- # - або Повністю кваліфіковане ім'я класу маршрутизатора.
- # Клас має розширювати akka.routing.CustomRouterConfig та
- # мати публічний конструктор з com.typesafe.config.Config
- # та опціональним праметром akka.actor.DynamicAccess.
- # - по замовчанню "from-code";
- # Чи буде актор трансформовано на маршрутизатор, визначаєтсья в коді
- # (Props.withRouter). Тип машрутизатора може бути перекритий в
- # конфігурації; вказуючи "from-code" означає, що будуть задіяні значення,
- # вказані в коді.
- # В випадку маршрутизації актори будуть машрутуватись в декілька способів:
- # - nr-of-instances: буде створювати стільки дітей
- # - routees.paths: буде маршрутизувати повідомлення по цьому шляху
- # з використанням ActorSelection, тобто не створюватиме дітей
- # - resizer: динамічно змінюване число маршрутів, як вказане нижче
- router = "from-code"
-
- # число дітей, що створюються в випадку маршрутизатора;
- # це налаштування ігнорується, якщо вказаний routees.paths
- nr-of-instances = 1
-
- # таймаут, використовуваний для маршрутизаторів, що містять
- # майбутні виклики
- within = 5 seconds
-
- # число віртуальних вузлів на один вузол, для маршрутизатора
- # consistent-hashing
- virtual-nodes-factor = 10
-
- tail-chopping-router {
- # інтервал є відтинком часу для пересилання на наступний маршрут
- interval = 10 milliseconds
- }
-
- routees {
- # Альтернативно для надання nr-of-instances, як ви можете вказати
- # повні шляхи до тих акторів, до яких потрібна маршрутизація.
- # Це налаштування має перевагу над nr-of-instances
- paths = []
- }
-
- # Щоб використати окремий диспечер для маршрутів пула, ви можете
- # визначити конфігурацію диспечера поряд з ім'ям властивості
- # 'pool-dispatcher' в розділі розгортання маршрутизатора.
- # Наприклад:
- # pool-dispatcher {
- # fork-join-executor.parallelism-min = 5
- # fork-join-executor.parallelism-max = 5
- # }
-
- # Маршрутизатори з динамічним числом маршрутів; ця можливість
- # включена через включення (частин) цього розділу до розгортання
- resizer {
-
- enabled = off
-
- # Найменьше число маршрутів, що має утримувати маршутизатор.
- lower-bound = 1
-
- # Найбільше число маршрутів, що має утримувати маршрутизатор.
- # Має бути більше або рівне lower-bound.
- upper-bound = 10
-
- # Рівень відсікання, що використовується для обчислення, чи маршрут
- # слід визнати навантаженим (під тиском). Реалізації залежать від
- # цього значення (по замовчанню 1).
- # 0: число маршрутів, що наразі обробляють повідомлення.
- # 1: число маршрутів, що наразі мають деякі повідомлення в
- # поштовій скриньці.
- # > 1: число маршрутів, з щонайменьше сконфігурованим в
- # pressure-thresholdmessages числом повідомлень в поштовій скриньці.
- # Зауважте, що очікуваний розмір скриньки UnboundedMailbox є
- # операцією O(N).
- pressure-threshold = 1
-
- # Відсоток розміру зростання, коли всі маршрути зайняті.
- # Наприклад, 0.2 буде давати зростання 20% (округлене доверху),
- # тобто якщо поточна ємність 6, це дасть запит на на 2 додаткові маршрути.
- rampup-rate = 0.2
-
- # Мінімальна доля зайнятих маршрутів, перед відмовою від маршрутів.
- # Наприклад, якщо це 0.3, тоді ми видаляємодеяки маршрути, коли
- # меньше ніж 30% маршрутів зайняті, тобто, якщо поточна ємність є 10,
- # та 3 зайняті, тоді ємність не змінюється, але якщо зайняті 2 або меньше,
- # тоді ємність буде зменшена.
- # Використовуйте 0.0 або від'ємне значення, щоб уникнути видалення
- # маршрутів.
- backoff-threshold = 0.3
-
- # Доля маршрутів, що будуть видалені, коли їх число досягне
- # backoff-Threshold.
- # Наприклад, 0.1 буде зменьшувати на 10% (округлене), тобто, якщо
- # поточна місткість 9, буде запит на зменшення 1 маршруту.
- backoff-rate = 0.1
-
- # Число повідомлень між операціями зменшення.
- # Використовуйте 1, щоб змінювати розмір перед кожним повідомленням.
- messages-per-resize = 10
- }
-
- # Маршрутизатори зі змінним числом маршрутів на основі
- # метрик продуктивності.
- # Ця можливість доступна черезвключення (частин) цього розділу в
- # розгортання, не може бути включене разом з підлаштуванням розміру
- # по замовчанню.
- optimal-size-exploring-resizer {
-
- enabled = off
-
- # Найменьше число маршрутів, що повинен мати маршрутизатор.
- lower-bound = 1
-
- # Найбільше число маршрутів, що повинен мати маршрутизатор.
- # Має бути більше або рівне lower-bound.
- upper-bound = 10
-
- # Вірогідність виконання скочування, коли всі маршрути зайняті
- # під час дослідшення.
- chance-of-ramping-down-when-full = 0.2
-
- # Інтервал між кожною спробою зміни розміра
- action-interval = 5s
-
- # Якщо маршрути не були повністю використані (тобто не всі зайняті)
- # для такої довжини, відбудеться спроба зменшити пул.
- downsize-after-underutilized-for = 72h
-
- # Дослідження тривалості, відношення між найбільшим кроком, та
- # поточним розміром пула. Тобто, якщо поточний пул має розмір 50,
- # та explore-step-size 0.1, максимальний розмір зміни пула на протязі
- # дослідження буде +- 5
- explore-step-size = 0.1
-
- # Вірогідність виконання дослідження, або оптимізації.
- chance-of-exploration = 0.4
-
- # Коли відбувається скорочення після довгого недовикористання,
- # процес буде зменшувати пул до найбільшого вуикористання, помножене
- # на коефіцієнт. Цей коефіцієнт зменшення визначає новий розмір пула
- # в порівнянні з найбільшим навантаженням.
- # Тобто, якщо найбільше навантаження 10, та коефіціент зменшення
- # є 0.8, пул буде зменшений до 8
- downsize-ratio = 0.8
-
- # При оптимізації береться до уваги тільки зміну до поточного розміру.
- # Це число вказує, скільки поточних розмірів буде прийматись до уваги.
- optimization-range = 16
-
- # Вага останньої метрики над старими метриками при зборі метрик
- # продуктивності.
- # Тобто, якщо остання швидкість обробки є 10мс на повідомлення,
- # з роміром пула 5, та якщо нова зібрана швидкість обробки 6мс
- # на повідомлення при розмірі пула 5. Тоді беручи вагу 0.3, метріка,
- # що представляє пул розміром 5 буде 6 * 0.3 + 10 * 0.7, тобто 8.8мс.
- # Очевидно, це число має бути між 0 та 1.
- weight-of-latest-metric = 0.5
- }
- }
-
- /IO-DNS/inet-address {
- mailbox = "unbounded"
- router = "consistent-hashing-pool"
- nr-of-instances = 4
- }
- }
-
- default-dispatcher {
- # Має бути одне з наступного:
- # Dispatcher, PinnedDispatcher, або FQCN до наслідуючого класу
- # MessageDispatcherConfigurator з публічним конструктором з параметрами
- # com.typesafe.config.Config та akka.dispatch.DispatcherPrerequisites.
- # PinnedDispatcher має бути використаний разом з executor=thread-pool-executor.
- type = "Dispatcher"
-
- # Який різновид ExecutorService використовувати з цім диспечером
- # Прийнятні опції:
- # - "default-executor" протребує розділу "default-executor"
- # - "fork-join-executor" протребує розділу "fork-join-executor"
- # - "thread-pool-executor" потребує розділу "thread-pool-executor"
- # - FQCN класу, що розширює ExecutorServiceConfigurator
- executor = "default-executor"
-
- # Це буде використовуватись, якщо ви встановили"executor="default-executor"".
- # Якщо ActorSystem створена з наданим ExecutionContext, цей
- # ExecutionContext буде використаний як екзекутор по замовчанню для всіх
- # диспечерів в ActorSystem, сконфігурованого для executor="default-executor".
- # Зауважте, що "default-executor" є значенням по замовчанню для екзекутора,
- # та, таким чином, використовується, якщо не вказане інакше.
- # Якщо не надано ExecutionContext, використовується екзекутор,
- # сконфігурований як "fallback".
- default-executor {
- fallback = "fork-join-executor"
- }
-
- # Це буде використане, якщо ви встановили "executor="fork-join-executor""
- fork-join-executor {
- # Мінімальне число потоків, щоб перевершіти factor-based паралелізм
- parallelism-min = 8
-
- # Фактор паралелізму, що використовується для визначеннярозміру пула потоків
- # з використанням наступної формули: ceil(available processors * factor).
- # Отриманий розмір потім обмежується parallelism-min та parallelism-max.
- parallelism-factor = 3.0
-
- # Максимальне число потоків, щоб покрити фактор паралелізму
- parallelism-max = 64
-
- # Встановлюється в "FIFO", щоб встановити режим черги, "poll" або "LIFO"
- # для використання дисціпліни стеку, що може "pop".
- task-peeking-mode = "FIFO"
- }
-
- # Це використовується, якщо встановлене "executor = "thread-pool-executor""
- thread-pool-executor {
- # Час Keep-alive time для потоків
- keep-alive-time = 60s
-
- # Мінімальне число потоків, щоб обмежити фактор числа ядер
- core-pool-size-min = 8
-
- # Фактор розміру пула ядер, використовується для визначення розміру
- # потоків ядра за такою формулою: ceil(доступні_процесори*фактор).
- # Отриманий розміробмежується значеннями core-pool-size-min та
- # core-pool-size-max.
- core-pool-size-factor = 3.0
-
- # Максимальне число потоків, щоб обмежити фактор числа ядер
- core-pool-size-max = 64
-
- # Мінімальне число потоків, щоб обмежити фактор максимального числа
- # (якщо використовується обмежена черга завдань)
- max-pool-size-min = 8
-
- # Максимальне число потоків (якщо використовується обмежена черга завдань)
- # що обчислюється таким чином: ceil(available_processors*factor)
- max-pool-size-factor = 3.0
-
- # Максимальне число потоків, щоб обежити число фактор числа потоків
- # (якщо використовується обмежена черга завдань)
- max-pool-size-max = 64
-
- # Вказує обмежену місткість чарги завдать (< 1 == необмежено)
- task-queue-size = -1
-
- # Вказує, який тип черги завдань використовувати, може бути "array" або
- # "linked" (по замовчанню)
- task-queue-type = "linked"
-
- # Дозволяє таймаут потоків ядра
- allow-core-timeout = on
- }
-
- # Як багато часу буде чекати диспечер нового актора, доки він завершиться
- shutdown-timeout = 1s
-
- # Пропускна здібність. Число повідомлень,що обробляються пакетом,
- # перед тим, як потік повернеться до пула. 1 означає максимальну чесність.
- throughput = 5
-
- # Останній строк для диспечера, 0 або від'ємне означає відсутність
- throughput-deadline-time = 0ms
-
- # Для BalancingDispatcher: якщо балансуючий диспечер повинен спробувати
- # планувати байдикуючі актори з тим же диспечером, коли надходить
- # повідомлення, та диспечери ExecutorService ще не повністю навантажені.
- attempt-teamwork = on
-
- # Якщо диспечер потребує особливого типу поштової скриньки, тут вказується
- # FQCN; насправді створена поштова скринька буде підтипом цього типа.
- # Порожній рядок вказує, що можливість не потрібна.
- mailbox-requirement = ""
- }
-
- default-mailbox {
- # FQCN MailboxType. Клас FQCN мусить мати публічний конструктор з
- # параметрами (akka.actor.ActorSystem.Settings, com.typesafe.config.Config).
- mailbox-type = "akka.dispatch.UnboundedMailbox"
-
- # Коли поштова скринька обмежена, вона використовує це налаштування для
- # визначення власної ємності. Запроваджене значення має бути додатним.
- # ЗАУВАЖЕННЯ:
- # До версії 2.1 тип поштової скриньки визначався на основі цього налаштування;
- # тепер це не так, тип треба вказувати явно як обмежена поштова скринька
- mailbox-capacity = 1000
-
- # Якщо скринька обмежена, тоді цей таймаут для ставлення в чергу, коли
- # скринька повна. Від'ємні значення вказують безкінечний таймаут,
- # що треба уникати, бо це викликає ризик ситуації глухого кута (зависання).
- mailbox-push-timeout-time = 10s
-
- # Для актора зі Stash: Ємність сховку по замовчанню.
- # Якщо від'ємне (або нуль), тоді використовується необмежений сховок
- # (по замовчанню), якщо додатне, тоді сховок обмежений, та ємність
- # визначається значенням цієї властивості
- stash-capacity = -1
- }
-
- mailbox {
- # Відображення між семантикою черги повідомлень та конфігурацією скриньки.
- # Використовується akka.dispatch.RequiresMessageQueue[T] для астановлення
- # різних типів поштових скриньок для акторів.
- # Якщо ваш Actor реалізує RequiresMessageQueue[T], тоді при створенні
- # примірника такого актора тип його скриньки буде визначатись з огляду
- # на конфігурацію поштової скриньки через T в цій мапі
- requirements {
- "akka.dispatch.UnboundedMessageQueueSemantics" =
- akka.actor.mailbox.unbounded-queue-based
- "akka.dispatch.BoundedMessageQueueSemantics" =
- akka.actor.mailbox.bounded-queue-based
- "akka.dispatch.DequeBasedMessageQueueSemantics" =
- akka.actor.mailbox.unbounded-deque-based
- "akka.dispatch.UnboundedDequeBasedMessageQueueSemantics" =
- akka.actor.mailbox.unbounded-deque-based
- "akka.dispatch.BoundedDequeBasedMessageQueueSemantics" =
- akka.actor.mailbox.bounded-deque-based
- "akka.dispatch.MultipleConsumerSemantics" =
- akka.actor.mailbox.unbounded-queue-based
- "akka.dispatch.ControlAwareMessageQueueSemantics" =
- akka.actor.mailbox.unbounded-control-aware-queue-based
- "akka.dispatch.UnboundedControlAwareMessageQueueSemantics" =
- akka.actor.mailbox.unbounded-control-aware-queue-based
- "akka.dispatch.BoundedControlAwareMessageQueueSemantics" =
- akka.actor.mailbox.bounded-control-aware-queue-based
- "akka.event.LoggerMessageQueueSemantics" =
- akka.actor.mailbox.logger-queue
- }
-
- unbounded-queue-based {
- # FQCN MailboxType, клас FQCN мусить мати публічний конструктор
- # з параметрами (akka.actor.ActorSystem.Settings,
- # com.typesafe.config.Config).
- mailbox-type = "akka.dispatch.UnboundedMailbox"
- }
-
- bounded-queue-based {
- # FQCN MailboxType, клас FQCN мусить мати публічний конструктор з
- # параметрами (akka.actor.ActorSystem.Settings,
- # com.typesafe.config.Config).
- mailbox-type = "akka.dispatch.BoundedMailbox"
- }
-
- unbounded-deque-based {
- # FQCN MailboxType, клас FQCN мусить мати публічний конструктор з
- # параметрами (akka.actor.ActorSystem.Settings,
- # com.typesafe.config.Config).
- mailbox-type = "akka.dispatch.UnboundedDequeBasedMailbox"
- }
-
- bounded-deque-based {
- # FQCN MailboxType, клас FQCN мусить мати публічний конструктор з
- # параметрами (akka.actor.ActorSystem.Settings,
- # com.typesafe.config.Config).
- mailbox-type = "akka.dispatch.BoundedDequeBasedMailbox"
- }
-
- unbounded-control-aware-queue-based {
- # FQCN MailboxType, клас FQCN мусить мати публічний конструктор з
- # параметрами (akka.actor.ActorSystem.Settings,
- # com.typesafe.config.Config).
- mailbox-type = "akka.dispatch.UnboundedControlAwareMailbox"
- }
-
- bounded-control-aware-queue-based {
- # FQCN MailboxType, клас FQCN мусить мати публічний конструктор з
- # параметрами (akka.actor.ActorSystem.Settings,
- # com.typesafe.config.Config).
- mailbox-type = "akka.dispatch.BoundedControlAwareMailbox"
- }
-
- # LoggerMailbox буде вичерпувати всі повідомлення в поштовій скриньці
- # при завершенні системи, та доставляти їх до StandardOutLogger.
- # Не змінюйте це, якщо не знаєте, що робите.
- logger-queue {
- mailbox-type = "akka.event.LoggerMailboxType"
- }
- }
-
- debug {
- # вмикає функцію Actor.loggable(), що журналює всі отримані повідомлення
- # на рівні DEBUG, дивіться розділ “тестування системи акторів” документації
- # Akka на http://akka.io/docs
- receive = off
-
- # вмикає журналювання DEBUG всіх AutoReceiveMessages (Kill, PoisonPill etc)
- autoreceive = off
-
- # вмикає журналювання DEBUG змін життєвого циклу акторів
- lifecycle = off
-
- # вмикає журналювання DEBUG всіх LoggingFSM для подій, переходів та таймерів
- fsm = off
-
- # вмикає журналювання DEBUG змін підписки на eventStream
- event-stream = off
-
- # вмикає журналювання DEBUG необроблених повідомлень
- unhandled = off
-
- # вмикає журналювання WARN невірно сконфігурованих маршрутів
- router-misconfiguration = off
- }
-
- # Входження для підключуваних серіалізаторів та їх прив'язок.
- serializers {
- java = "akka.serialization.JavaSerializer"
- bytes = "akka.serialization.ByteArraySerializer"
- }
-
- # Клас для прив'язок Serializer. Вам треба вказати тільки ім'я інтерфейса
- # або абстрактного базового класу повідомлень. В випадку неоднозначності
- # використовується найбільш специфічний сконфігурований клас, або видається
- # попередження, що обирається “перший зустрічний”.
- #
- # Щоб відключити один з серіалізаторів по замовчанню, встановіть його клас
- # в "none", як в "java.io.Serializable" = none
- serialization-bindings {
- "[B" = bytes
- "java.io.Serializable" = java
- }
-
- # Журнал попереджень, коли серіалізація Java по замовчаннювикористовується для
- # серіалізації повідомлень. Серіалізатор по замовчанню використовує Java, що
- # не дуже оптимально, та не повинно використовуватись в виробничому оточенні,
- # тільки якщо вам не байдужа продуктивність. В такому випадку ви можете
- # перемикнути це в off.
- warn-about-java-serializer-usage = on
-
- # Конфігурація простору імен ідентифікаторів сериалізації.
- # Кожна реалізація сериалізації повинна мати входження в наступному форматі:
- # `akka.actor.serialization-identifiers."FQCN" = ID`
- # де `FQCN` повністю кваліффіковане ім'я класа реалізації сериалізації,
- # та `ID` є глобально унікальне число-ідентифікатор серіалізатора.
- # Значення ідентифікаторів починаючи з 0 до 16 зарезервовані для
- # внутрішнього використання Akka.
- serialization-identifiers {
- "akka.serialization.JavaSerializer" = 1
- "akka.serialization.ByteArraySerializer" = 4
- }
-
- # Елементи конфігурації, що використовуються методами akka.actor.ActorDSL._
- dsl {
- # Максимальний розмір черги акторів, створених newInbox(); це захищає
- # проти збойних програм, що використовують select(), та постійно втрачають
- # повідомлення
- inbox-size = 1000
-
- # Таймаут по замовчанню для опкерацій як Inbox.receive, та інших
- default-timeout = 5s
- }
- }
-
- # Використовується для встановлення поведінки планувальника.
- # Зміна значень по замовчанню може значно змінити поведінку системи, так що
- # переконайтесь, що ви знаєте, що робите! Дивіться розділ Scheduler документації
- # Akka Documentation щодо деталей.
- scheduler {
- # LightArrayRevolverScheduler використовується в системі як планувальник по замовчанню.
- # Він не виконує завдання в певний час, але кожиний тік він виконує все,
- # що накопичилось. Ви можете збільшити або зменшити точність часу виконання,
- # вказуючі більші або меньші проміжки часу тіків. Якщо ви плануєте багато
- # завдань, ви можете розглянути можливість збільшення тіків на оберт.
- # Зауважте, що може пройти 1 тік для зупинки таймера, так що встановлення
- # тривалості тіка в більше значення зробить завершення системи акторів
- # більш тривалим.
- tick-duration = 10ms
-
- # Таймер використовує циклічний оберт пачок, щоб зберігати завдання таймера.
- # Це має бути встановлене таким чином, щоб більшість таймаутів планування
- # (для високої частоти планування) було б коротше, ніж один оберт
- # (ticks-per-wheel * ticks-duration)
- # ЦЕ МАЄ БУТИ СТУПІНЬ ДВОХ!
- ticks-per-wheel = 512
-
- # Це налаштування обирає реалізацію таймера, що буде завантажена системою
- # при запуску.
- # Наданий тут клас має реалізовувати інтерфейс akka.actor.Scheduler
- # та пропонувати публічний конструктор, що приймає три аргументи:
- # 1) com.typesafe.config.Config
- # 2) akka.event.LoggingAdapter
- # 3) java.util.concurrent.ThreadFactory
- implementation = akka.actor.LightArrayRevolverScheduler
-
- # При завершенні планувальника типово є потік, що треба зупинити, та цей
- # таймаут визначає, як довго чекати, коли це трапиться. В випадку таймауту
- # завершення система акторів буде продовжувати без виконання, можливо все
- # ставлячи завдання в чергу.
- shutdown-timeout = 5s
- }
-
- io {
-
- # По замовчанню цикли select виконуються в виділених потоках, таким чином
- # використовуючи PinnedDispatcher
- pinned-dispatcher {
- type = "PinnedDispatcher"
- executor = "thread-pool-executor"
- thread-pool-executor.allow-core-timeout = off
- }
-
- tcp {
-
- # Число селекторів для очищення обслуговуваних каналів; кожний з них
- # буде використовувати один цикл select на selector-dispatcher.
- nr-of-selectors = 1
-
- # Максимальне число відкритих каналів, підтримуваних цім TCP модулем;
- # не існує внутрішнього загального ліміту, це налаштування призначене
- # для запобігання DoS, обмежуючи число конкурентно під'єднаних клієнтів.
- # Також зауважте, що це "софт" ліміт; в деяких випадках реалізація
- # буде сприймати на декілька з'єднань більше, або на декілька меньше,
- # ніж сконфігуроване тут число. Має бути цілим > 0 або "unlimited".
- max-channels = 256000
-
- # При спробі призначити нове з'єднання до селектора, та цей селектор
- # вичерпав ємність, спробувати новий селекто, та присвоювати стільки
- # разів перед тим, як облишити спроби
- selector-association-retries = 10
-
- # Максимальне число з'єднань, що сприймаються за один раз,
- # вищі значення зменшують затримки, нижчі значення збільшують чесність
- # на worker-dispatcher
- batch-accept-limit = 10
-
- # Число байт на один прямий буфер в пулі, що використовується для
- # читання або запису даних з ядра.
- direct-buffer-size = 128 KiB
-
- # Максимальне число різних буферів, що утримуються в прямому пулі буферів
- # для повторного використання.
- direct-buffer-pool-limit = 1000
-
- # Проміжок часу, що актор з'єднання чекає повідомлення `Register` від
- # його командуючого, перед обірванням з'єднання.
- register-timeout = 5s
-
- # Максимальне число байтів, доставлених в повідомленні `Received`. Перед
- # тим, як читати додаткові дані з мережі, актор з'єднання спробує
- # робити іншу роботу.
- # Призначення цього налаштування є надати меньший ліміт, ніж сконфігурований
- # розмір буфера отримання. Якщо використовується значення 'unlimited'
- # буде спроба прочитати всі дані з вхідного буфера.
- max-received-message-size = unlimited
-
- # Дозволяє гарно налаштувати журналювання того, що відбувається в реалізації.
- # Будьте уважні, це може створювати більше одного запису на повідомлення,
- # надісланого до акторів через tcp реалізацію.
- trace-logging = off
-
- # Повністю кваліфікований шлях конфігурації, що містить конфігурацію
- # маршрутизатора для виконання викликів select() в селекторах
- selector-dispatcher = "akka.io.pinned-dispatcher"
-
- # Повнісню кваліфікований шлях конфігурації, що містить конфігурацію
- # диспечера для робочих акторів читання/запису
- worker-dispatcher = "akka.actor.default-dispatcher"
-
- # Повністю кваліфікований шлях конфігурації, що містить конфігурацію
- # диспечера для акторів менеджмента селекторів
- management-dispatcher = "akka.actor.default-dispatcher"
-
- # Повністю кваліфікований шлях конфігурації, що містить конфігурацію
- # диспечера, на якому плануються завдання файлового IO
- file-io-dispatcher = "akka.actor.default-dispatcher"
-
- # Максимальне число байт (або "unlimited"), що передаються одним пакунком
- # при використанні комнади `WriteFile`, що використовує `FileChannel.transferTo`
- # для пересилки файлів в TCP сокет. На деяких OS, як Linux `FileChannel.transferTo`
- # може надовго блокувати, коли мережева передача IO швидша, ніж файлове IO.
- # Зменьшення знаяення може покращити чесність, тоді як збільшення може
- # покращити пропускну спроможність.
- file-io-transferTo-limit = 512 KiB
-
- # Число разів, скільки треба повторити виклик `finishConnect` після повідомлення
- # про OP_CONNECT. Повтори потрібні, якщоповідомлення OP_CONNECT не очікує, що
- # `finishConnect` буде успішним, що відбувається під Android.
- finish-connect-retries = 5
-
- # Під Windows обрив з'єднання детектуються ненадійно, якщо тільки OP_READ не
- # зареєстроване на селекторі _після_ того, як з'єднання було скинуте. Цей
- # обхідний маневр дозволяє OP_CONNECT, що змушує обрив бути помітним на Windows.
- # Включення цієї опції на інших платформах, ніж Windows, призводить до
- # різноманітних збоїв та невизначеної поведінки.
- # Можливі значення цього ключа є on, off та auto, де auto буде включати
- # обхід, якщо автоматично визначається Windows.
- windows-connection-abort-workaround-enabled = off
- }
-
- udp {
-
- # Число селекторів, щоб скинути обслуговувані канали; кожний з
- # них буде використовувати один цикл select на selector-dispatcher.
- nr-of-selectors = 1
-
- # Максимальне число відкритих каналів, підтримуваних цім UDP модулем.
- # Загалом UDP не потребує великого числа каналів, таким чином, рекомендовано
- # утримувати цей параметр малим.
- max-channels = 4096
-
- # Цикл select може використовуватись в двох режимах:
- # - встановлення "infinite" буде обирати без таймаута, прогинаючи потіік
- # - встановлення додатний таймаута буде викликати обмежений select,
- # дозволяючи використовувати один потік між декількома селекторами
- # (в цьому випадку вам треба використовувати різні конфігурації для
- # selector-dispatcher, тобто "type=Dispatcher" з розміром 1)
- # - встановлення його в нуль означає опит, тобто виклики selectNow()
- select-timeout = infinite
-
- # При намаганні присвоїти нове з'єднання селектору, але обраний селектор
- # досяг межі ємності, повторити вибір та присвоєння таке число разів
- # перед припиненням спроб
- selector-association-retries = 10
-
- # Максимальне число датаграм, що читаються за один раз, більші значення
- # зменшують затримки, меньші значення збільшують чесність на
- # worker-dispatcher
- receive-throughput = 3
-
- # Число байт на один прямий буфер в пулі, що використовується для читання
- # або запису мережевих даних з ядра.
- direct-buffer-size = 128 KiB
-
- # Максимальне число прямих буферів, що утримуються в пулі прямих буферів
- # для повторного використання.
- direct-buffer-pool-limit = 1000
-
- # Максимальне число байт, доставлених повідомленням `Received`. Перед
- # тим, як читати більше даних з мережевого з'єднання, актор з'єднання спробує
- # виконати якусь іншу роботу.
- received-message-size-limit = unlimited
-
- # Дозволяє гарно налаштувати журналювання щодо того, що відбувається в реалізації.
- # Майте на увазі, що до журналу можуть потрапляти більше одного запису на
- # повідомлення, надіслане актору по tcp реалізації.
- trace-logging = off
-
- # Повністю кваліфікований шлях конфігурації, що містить конфігурацію диспечера,
- # що буде використаний для викликів select() в селекторах
- selector-dispatcher = "akka.io.pinned-dispatcher"
-
- # Повністю кваліфікований шлях конфігурації, що містить конфігурацію диспечера
- # для робочих акторів читання/запису
- worker-dispatcher = "akka.actor.default-dispatcher"
-
- # Повністю кваліфікований шлях конфігурації, що містить конфігурацію диспечера
- # для акторів менеджмента селекторів
- management-dispatcher = "akka.actor.default-dispatcher"
- }
-
- udp-connected {
-
- # Число селекторів, щоб скинути обслуговувані канали; кожний з
- # них буде використовувати один цикл select на selector-dispatcher.
- nr-of-selectors = 1
-
- # Максимальне число відкритих каналів, підтримуваних цім UDP модулем.
- # Загалом, UDP не потребує великого числа каналів, і, таким чином,
- # рекомендовано утримувати це налаштування малим.
- max-channels = 4096
-
- # Цикл select може використовуватись в двох режимах:
- # - встановлення "infinite" буде обирати без таймауту, прогинаючи потік
- # - встановлення додатний таймаута буде викликати обмежений select,
- # дозволяючи використовувати один потік між декількома селекторами
- # (в цьому випадку вам треба використовувати різні конфігурації для
- # selector-dispatcher, тобто "type=Dispatcher" з розміром 1)
- # - встановлення його в нуль означає опит, тобто виклики selectNow()
- select-timeout = infinite
-
- # При спробі призначити нове з'єднання селектору, та обраний селектор
- # вичерпав повну ємність, повторювати вибір селектора та присвоєння
- # вказане число раз, перед припиненням спроб
- selector-association-retries = 10
-
- # Максимальне число датаграм, що читаються за один раз, вищі значення
- # зменшують затримки, меньші значення збільшують чесність на
- # worker-dispatcher
- receive-throughput = 3
-
- # Число байт на прямий буфер в пулі, що використовуються для читання та
- # запису мережевих даних з ядра.
- direct-buffer-size = 128 KiB
-
- # Максимальне число прямих буферів, що утримуються в пулі прямих буферів
- # для повторного використання.
- direct-buffer-pool-limit = 1000
-
- # Максимальне число байтів, доставлених через повідомлення `Received`.
- # Перед тим, як читати додаткові байти з мережі, актор з'єднання спробує
- # виконати інші завдання.
- received-message-size-limit = unlimited
-
- # Дозволяє гарно налаштувати журналювання щодо того, що відбувається зсеердини
- # реалізації. Майте на увазі, що до журналу може потрапити більше одного
- # запису на повідомленяя, надіслане актору по tcp реалізації.
- trace-logging = off
-
- # Повністю кваліфікований шлях конфігурації, що містить конфігурацію диспечера,
- # що буде використаний для викликів select() в селекторах
- selector-dispatcher = "akka.io.pinned-dispatcher"
-
- # Повністю кваліфікований шлях конфігурації, що містить конфігурацію диспечера
- # для робочих акторів читання/запису
- worker-dispatcher = "akka.actor.default-dispatcher"
-
- # Повністю кваліфікований шлях конфігурації, що містить конфігурацію диспечера
- # для акторів менеджменту селекторів
- management-dispatcher = "akka.actor.default-dispatcher"
- }
-
- dns {
- # Повністю кваліфікований шлях конфігурації, що містить конфігурацію диспечера
- # для акторів менеджменту та ресолвера for маршрутів.
- # Для справжньої конфігурації маршрутизаторів дивіться akka.actor.deployment./IO-DNS/*
- dispatcher = "akka.actor.default-dispatcher"
-
- # Ім'я підконфігурації по шляху akka.io.dns, дивіться inet-address нижче
- resolver = "inet-address"
-
- inet-address {
- # Має реалізувати akka.io.DnsProvider
- provider-object = "akka.io.InetAddressDnsProvider"
-
- # Ці TTL встановлені по замовчанню для java 6
- positive-ttl = 30s
- negative-ttl = 10s
-
- # Як часто очищати застарілі елементи кешу.
- # Зауважте, що цей інтервал не має нічого загального з TTL
- cache-cleanup-interval = 120s
- }
- }
- }
-
-
- }
akka-agent
- ####################################
- # Akka Agent Reference Config File #
- ####################################
-
- # This is the reference config file that contains all the default settings.
- # Make your edits/overrides in your application.conf.
-
- akka {
- agent {
-
- # The dispatcher used for agent-send-off actor
- send-off-dispatcher {
- executor = thread-pool-executor
- type = PinnedDispatcher
- }
-
- # The dispatcher used for agent-alter-off actor
- alter-off-dispatcher {
- executor = thread-pool-executor
- type = PinnedDispatcher
- }
- }
- }
akka-camel
- ####################################
- # Akka Camel Reference Config File #
- ####################################
-
- # This is the reference config file that contains all the default settings.
- # Make your edits/overrides in your application.conf.
-
- akka {
- camel {
- # FQCN of the ContextProvider to be used to create or locate a CamelContext
- # it must implement akka.camel.ContextProvider and have a no-arg constructor
- # the built-in default create a fresh DefaultCamelContext
- context-provider = akka.camel.DefaultContextProvider
-
- # Whether JMX should be enabled or disabled for the Camel Context
- jmx = off
- # enable/disable streaming cache on the Camel Context
- streamingCache = on
- consumer {
- # Configured setting which determines whether one-way communications
- # between an endpoint and this consumer actor
- # should be auto-acknowledged or application-acknowledged.
- # This flag has only effect when exchange is in-only.
- auto-ack = on
-
- # When endpoint is out-capable (can produce responses) reply-timeout is the
- # maximum time the endpoint can take to send the response before the message
- # exchange fails. This setting is used for out-capable, in-only,
- # manually acknowledged communication.
- reply-timeout = 1m
-
- # The duration of time to await activation of an endpoint.
- activation-timeout = 10s
- }
-
- #Scheme to FQCN mappings for CamelMessage body conversions
- conversions {
- "file" = "java.io.InputStream"
- }
- }
- }
akka-cluster
- ######################################
- # Akka Cluster Reference Config File #
- ######################################
-
- # This is the reference config file that contains all the default settings.
- # Make your edits/overrides in your application.conf.
-
- akka {
-
- cluster {
- # Initial contact points of the cluster.
- # The nodes to join automatically at startup.
- # Comma separated full URIs defined by a string on the form of
- # "akka.tcp://system@hostname:port"
- # Leave as empty if the node is supposed to be joined manually.
- seed-nodes = []
-
- # how long to wait for one of the seed nodes to reply to initial join request
- seed-node-timeout = 5s
-
- # If a join request fails it will be retried after this period.
- # Disable join retry by specifying "off".
- retry-unsuccessful-join-after = 10s
-
- # Should the 'leader' in the cluster be allowed to automatically mark
- # unreachable nodes as DOWN after a configured time of unreachability?
- # Using auto-down implies that two separate clusters will automatically be
- # formed in case of network partition.
- # Disable with "off" or specify a duration to enable auto-down.
- auto-down-unreachable-after = off
-
- # Time margin after which shards or singletons that belonged to a downed/removed
- # partition are created in surviving partition. The purpose of this margin is that
- # in case of a network partition the persistent actors in the non-surviving partitions
- # must be stopped before corresponding persistent actors are started somewhere else.
- # This is useful if you implement downing strategies that handle network partitions,
- # e.g. by keeping the larger side of the partition and shutting down the smaller side.
- # It will not add any extra safety for auto-down-unreachable-after, since that is not
- # handling network partitions.
- # Disable with "off" or specify a duration to enable.
- down-removal-margin = off
-
- # By default, the leader will not move 'Joining' members to 'Up' during a network
- # split. This feature allows the leader to accept 'Joining' members to be 'WeaklyUp'
- # so they become part of the cluster even during a network split. The leader will
- # move 'WeaklyUp' members to 'Up' status once convergence has been reached. This
- # feature must be off if some members are running Akka 2.3.X.
- # WeaklyUp is an EXPERIMENTAL feature.
- allow-weakly-up-members = off
-
- # The roles of this member. List of strings, e.g. roles = ["A", "B"].
- # The roles are part of the membership information and can be used by
- # routers or other services to distribute work to certain member types,
- # e.g. front-end and back-end nodes.
- roles = []
-
- role {
- # Minimum required number of members of a certain role before the leader
- # changes member status of 'Joining' members to 'Up'. Typically used together
- # with 'Cluster.registerOnMemberUp' to defer some action, such as starting
- # actors, until the cluster has reached a certain size.
- # E.g. to require 2 nodes with role 'frontend' and 3 nodes with role 'backend':
- # frontend.min-nr-of-members = 2
- # backend.min-nr-of-members = 3
- #.min-nr-of-members = 1
- }
-
- # Minimum required number of members before the leader changes member status
- # of 'Joining' members to 'Up'. Typically used together with
- # 'Cluster.registerOnMemberUp' to defer some action, such as starting actors,
- # until the cluster has reached a certain size.
- min-nr-of-members = 1
-
- # Enable/disable info level logging of cluster events
- log-info = on
-
- # Enable or disable JMX MBeans for management of the cluster
- jmx.enabled = on
-
- # how long should the node wait before starting the periodic tasks
- # maintenance tasks?
- periodic-tasks-initial-delay = 1s
-
- # how often should the node send out gossip information?
- gossip-interval = 1s
-
- # discard incoming gossip messages if not handled within this duration
- gossip-time-to-live = 2s
-
- # how often should the leader perform maintenance tasks?
- leader-actions-interval = 1s
-
- # how often should the node move nodes, marked as unreachable by the failure
- # detector, out of the membership ring?
- unreachable-nodes-reaper-interval = 1s
-
- # How often the current internal stats should be published.
- # A value of 0s can be used to always publish the stats, when it happens.
- # Disable with "off".
- publish-stats-interval = off
-
- # The id of the dispatcher to use for cluster actors. If not specified
- # default dispatcher is used.
- # If specified you need to define the settings of the actual dispatcher.
- use-dispatcher = ""
-
- # Gossip to random node with newer or older state information, if any with
- # this probability. Otherwise Gossip to any random live node.
- # Probability value is between 0.0 and 1.0. 0.0 means never, 1.0 means always.
- gossip-different-view-probability = 0.8
-
- # Reduced the above probability when the number of nodes in the cluster
- # greater than this value.
- reduce-gossip-different-view-probability = 400
-
- # Settings for the Phi accrual failure detector (http://www.jaist.ac.jp/~defago/files/pdf/IS_RR_2004_010.pdf
- # [Hayashibara et al]) used by the cluster subsystem to detect unreachable
- # members.
- # The default PhiAccrualFailureDetector will trigger if there are no heartbeats within
- # the duration heartbeat-interval + acceptable-heartbeat-pause + threshold_adjustment,
- # i.e. around 5.5 seconds with default settings.
- failure-detector {
-
- # FQCN of the failure detector implementation.
- # It must implement akka.remote.FailureDetector and have
- # a public constructor with a com.typesafe.config.Config and
- # akka.actor.EventStream parameter.
- implementation-class = "akka.remote.PhiAccrualFailureDetector"
-
- # How often keep-alive heartbeat messages should be sent to each connection.
- heartbeat-interval = 1 s
-
- # Defines the failure detector threshold.
- # A low threshold is prone to generate many wrong suspicions but ensures
- # a quick detection in the event of a real crash. Conversely, a high
- # threshold generates fewer mistakes but needs more time to detect
- # actual crashes.
- threshold = 8.0
-
- # Number of the samples of inter-heartbeat arrival times to adaptively
- # calculate the failure timeout for connections.
- max-sample-size = 1000
-
- # Minimum standard deviation to use for the normal distribution in
- # AccrualFailureDetector. Too low standard deviation might result in
- # too much sensitivity for sudden, but normal, deviations in heartbeat
- # inter arrival times.
- min-std-deviation = 100 ms
-
- # Number of potentially lost/delayed heartbeats that will be
- # accepted before considering it to be an anomaly.
- # This margin is important to be able to survive sudden, occasional,
- # pauses in heartbeat arrivals, due to for example garbage collect or
- # network drop.
- acceptable-heartbeat-pause = 3 s
-
- # Number of member nodes that each member will send heartbeat messages to,
- # i.e. each node will be monitored by this number of other nodes.
- monitored-by-nr-of-members = 5
-
- # After the heartbeat request has been sent the first failure detection
- # will start after this period, even though no heartbeat message has
- # been received.
- expected-response-after = 1 s
-
- }
-
- metrics {
- # Enable or disable metrics collector for load-balancing nodes.
- enabled = on
-
- # FQCN of the metrics collector implementation.
- # It must implement akka.cluster.MetricsCollector and
- # have public constructor with akka.actor.ActorSystem parameter.
- # The default SigarMetricsCollector uses JMX and Hyperic SIGAR, if SIGAR
- # is on the classpath, otherwise only JMX.
- collector-class = "akka.cluster.SigarMetricsCollector"
-
- # How often metrics are sampled on a node.
- # Shorter interval will collect the metrics more often.
- collect-interval = 3s
-
- # How often a node publishes metrics information.
- gossip-interval = 3s
-
- # How quickly the exponential weighting of past data is decayed compared to
- # new data. Set lower to increase the bias toward newer values.
- # The relevance of each data sample is halved for every passing half-life
- # duration, i.e. after 4 times the half-life, a data sample’s relevance is
- # reduced to 6% of its original relevance. The initial relevance of a data
- # sample is given by 1 – 0.5 ^ (collect-interval / half-life).
- # See http://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
- moving-average-half-life = 12s
- }
-
- # If the tick-duration of the default scheduler is longer than the
- # tick-duration configured here a dedicated scheduler will be used for
- # periodic tasks of the cluster, otherwise the default scheduler is used.
- # See akka.scheduler settings for more details.
- scheduler {
- tick-duration = 33ms
- ticks-per-wheel = 512
- }
-
- }
-
- # Default configuration for routers
- actor.deployment.default {
- # MetricsSelector to use
- # - available: "mix", "heap", "cpu", "load"
- # - or: Fully qualified class name of the MetricsSelector class.
- # The class must extend akka.cluster.routing.MetricsSelector
- # and have a public constructor with com.typesafe.config.Config
- # parameter.
- # - default is "mix"
- metrics-selector = mix
- }
- actor.deployment.default.cluster {
- # enable cluster aware router that deploys to nodes in the cluster
- enabled = off
-
- # Maximum number of routees that will be deployed on each cluster
- # member node.
- # Note that max-total-nr-of-instances defines total number of routees, but
- # number of routees per node will not be exceeded, i.e. if you
- # define max-total-nr-of-instances = 50 and max-nr-of-instances-per-node = 2
- # it will deploy 2 routees per new member in the cluster, up to
- # 25 members.
- max-nr-of-instances-per-node = 1
-
- # Maximum number of routees that will be deployed, in total
- # on all nodes. See also description of max-nr-of-instances-per-node.
- # For backwards compatibility reasons, nr-of-instances
- # has the same purpose as max-total-nr-of-instances for cluster
- # aware routers and nr-of-instances (if defined by user) takes
- # precedence over max-total-nr-of-instances.
- max-total-nr-of-instances = 10000
-
- # Defines if routees are allowed to be located on the same node as
- # the head router actor, or only on remote nodes.
- # Useful for master-worker scenario where all routees are remote.
- allow-local-routees = on
-
- # Use members with specified role, or all members if undefined or empty.
- use-role = ""
-
- }
-
- # Protobuf serializer for cluster messages
- actor {
- serializers {
- akka-cluster = "akka.cluster.protobuf.ClusterMessageSerializer"
- }
-
- serialization-bindings {
- "akka.cluster.ClusterMessage" = akka-cluster
- }
-
- serialization-identifiers {
- "akka.cluster.protobuf.ClusterMessageSerializer" = 5
- }
-
- router.type-mapping {
- adaptive-pool = "akka.cluster.routing.AdaptiveLoadBalancingPool"
- adaptive-group = "akka.cluster.routing.AdaptiveLoadBalancingGroup"
- }
- }
-
- }
akka-multi-node-testkit
- #############################################
- # Akka Remote Testing Reference Config File #
- #############################################
-
- # This is the reference config file that contains all the default settings.
- # Make your edits/overrides in your application.conf.
-
- akka {
- testconductor {
-
- # Timeout for joining a barrier: this is the maximum time any participants
- # waits for everybody else to join a named barrier.
- barrier-timeout = 30s
-
- # Timeout for interrogation of TestConductor’s Controller actor
- query-timeout = 5s
-
- # Threshold for packet size in time unit above which the failure injector will
- # split the packet and deliver in smaller portions; do not give value smaller
- # than HashedWheelTimer resolution (would not make sense)
- packet-split-threshold = 100ms
-
- # amount of time for the ClientFSM to wait for the connection to the conductor
- # to be successful
- connect-timeout = 20s
-
- # Number of connect attempts to be made to the conductor controller
- client-reconnects = 30
-
- # minimum time interval which is to be inserted between reconnect attempts
- reconnect-backoff = 1s
-
- netty {
- # (I&O) Used to configure the number of I/O worker threads on server sockets
- server-socket-worker-pool {
- # Min number of threads to cap factor-based number to
- pool-size-min = 1
-
- # The pool size factor is used to determine thread pool size
- # using the following formula: ceil(available processors * factor).
- # Resulting size is then bounded by the pool-size-min and
- # pool-size-max values.
- pool-size-factor = 1.0
-
- # Max number of threads to cap factor-based number to
- pool-size-max = 2
- }
-
- # (I&O) Used to configure the number of I/O worker threads on client sockets
- client-socket-worker-pool {
- # Min number of threads to cap factor-based number to
- pool-size-min = 1
-
- # The pool size factor is used to determine thread pool size
- # using the following formula: ceil(available processors * factor).
- # Resulting size is then bounded by the pool-size-min and
- # pool-size-max values.
- pool-size-factor = 1.0
-
- # Max number of threads to cap factor-based number to
- pool-size-max = 2
- }
- }
- }
- }
akka-remote
- #####################################
- # Akka Remote Reference Config File #
- #####################################
-
- # This is the reference config file that contains all the default settings.
- # Make your edits/overrides in your application.conf.
-
- # comments about akka.actor settings left out where they are already in akka-
- # actor.jar, because otherwise they would be repeated in config rendering.
-
- akka {
-
- actor {
-
- serializers {
- akka-containers = "akka.remote.serialization.MessageContainerSerializer"
- proto = "akka.remote.serialization.ProtobufSerializer"
- daemon-create = "akka.remote.serialization.DaemonMsgCreateSerializer"
- }
-
- serialization-bindings {
- "akka.actor.ActorSelectionMessage" = akka-containers
- "akka.remote.DaemonMsgCreate" = daemon-create
-
- # Since akka.protobuf.Message does not extend Serializable but
- # GeneratedMessage does, need to use the more specific one here in order
- # to avoid ambiguity.
- "akka.protobuf.GeneratedMessage" = proto
-
- # Since com.google.protobuf.Message does not extend Serializable but
- # GeneratedMessage does, need to use the more specific one here in order
- # to avoid ambiguity.
- # This com.google.protobuf serialization binding is only used if the class can be loaded,
- # i.e. com.google.protobuf dependency has been added in the application project.
- "com.google.protobuf.GeneratedMessage" = proto
-
- }
-
- serialization-identifiers {
- "akka.remote.serialization.ProtobufSerializer" = 2
- "akka.remote.serialization.DaemonMsgCreateSerializer" = 3
- "akka.remote.serialization.MessageContainerSerializer" = 6
- }
-
- deployment {
-
- default {
-
- # if this is set to a valid remote address, the named actor will be
- # deployed at that node e.g. "akka.tcp://sys@host:port"
- remote = ""
-
- target {
-
- # A list of hostnames and ports for instantiating the children of a
- # router
- # The format should be on "akka.tcp://sys@host:port", where:
- # - sys is the remote actor system name
- # - hostname can be either hostname or IP address the remote actor
- # should connect to
- # - port should be the port for the remote server on the other node
- # The number of actor instances to be spawned is still taken from the
- # nr-of-instances setting as for local routers; the instances will be
- # distributed round-robin among the given nodes.
- nodes = []
-
- }
- }
- }
- }
-
- remote {
-
- ### General settings
-
- # Timeout after which the startup of the remoting subsystem is considered
- # to be failed. Increase this value if your transport drivers (see the
- # enabled-transports section) need longer time to be loaded.
- startup-timeout = 10 s
-
- # Timout after which the graceful shutdown of the remoting subsystem is
- # considered to be failed. After the timeout the remoting system is
- # forcefully shut down. Increase this value if your transport drivers
- # (see the enabled-transports section) need longer time to stop properly.
- shutdown-timeout = 10 s
-
- # Before shutting down the drivers, the remoting subsystem attempts to flush
- # all pending writes. This setting controls the maximum time the remoting is
- # willing to wait before moving on to shut down the drivers.
- flush-wait-on-shutdown = 2 s
-
- # Reuse inbound connections for outbound messages
- use-passive-connections = on
-
- # Controls the backoff interval after a refused write is reattempted.
- # (Transports may refuse writes if their internal buffer is full)
- backoff-interval = 5 ms
-
- # Acknowledgment timeout of management commands sent to the transport stack.
- command-ack-timeout = 30 s
-
- # The timeout for outbound associations to perform the handshake.
- # If the transport is akka.remote.netty.tcp or akka.remote.netty.ssl
- # the configured connection-timeout for the transport will be used instead.
- handshake-timeout = 15 s
-
- # If set to a nonempty string remoting will use the given dispatcher for
- # its internal actors otherwise the default dispatcher is used. Please note
- # that since remoting can load arbitrary 3rd party drivers (see
- # "enabled-transport" and "adapters" entries) it is not guaranteed that
- # every module will respect this setting.
- use-dispatcher = "akka.remote.default-remote-dispatcher"
-
- ### Security settings
-
- # Enable untrusted mode for full security of server managed actors, prevents
- # system messages to be send by clients, e.g. messages like 'Create',
- # 'Suspend', 'Resume', 'Terminate', 'Supervise', 'Link' etc.
- untrusted-mode = off
-
- # When 'untrusted-mode=on' inbound actor selections are by default discarded.
- # Actors with paths defined in this white list are granted permission to receive actor
- # selections messages.
- # E.g. trusted-selection-paths = ["/user/receptionist", "/user/namingService"]
- trusted-selection-paths = []
-
- # Should the remote server require that its peers share the same
- # secure-cookie (defined in the 'remote' section)? Secure cookies are passed
- # between during the initial handshake. Connections are refused if the initial
- # message contains a mismatching cookie or the cookie is missing.
- require-cookie = off
-
- # Deprecated since 2.4-M1
- secure-cookie = ""
-
- ### Logging
-
- # If this is "on", Akka will log all inbound messages at DEBUG level,
- # if off then they are not logged
- log-received-messages = off
-
- # If this is "on", Akka will log all outbound messages at DEBUG level,
- # if off then they are not logged
- log-sent-messages = off
-
- # Sets the log granularity level at which Akka logs remoting events. This setting
- # can take the values OFF, ERROR, WARNING, INFO, DEBUG, or ON. For compatibility
- # reasons the setting "on" will default to "debug" level. Please note that the effective
- # logging level is still determined by the global logging level of the actor system:
- # for example debug level remoting events will be only logged if the system
- # is running with debug level logging.
- # Failures to deserialize received messages also fall under this flag.
- log-remote-lifecycle-events = on
-
- # Logging of message types with payload size in bytes larger than
- # this value. Maximum detected size per message type is logged once,
- # with an increase threshold of 10%.
- # By default this feature is turned off. Activate it by setting the property to
- # a value in bytes, such as 1000b. Note that for all messages larger than this
- # limit there will be extra performance and scalability cost.
- log-frame-size-exceeding = off
-
- # Log warning if the number of messages in the backoff buffer in the endpoint
- # writer exceeds this limit. It can be disabled by setting the value to off.
- log-buffer-size-exceeding = 50000
-
- ### Failure detection and recovery
-
- # Settings for the failure detector to monitor connections.
- # For TCP it is not important to have fast failure detection, since
- # most connection failures are captured by TCP itself.
- # The default DeadlineFailureDetector will trigger if there are no heartbeats within
- # the duration heartbeat-interval + acceptable-heartbeat-pause, i.e. 20 seconds
- # with the default settings.
- transport-failure-detector {
-
- # FQCN of the failure detector implementation.
- # It must implement akka.remote.FailureDetector and have
- # a public constructor with a com.typesafe.config.Config and
- # akka.actor.EventStream parameter.
- implementation-class = "akka.remote.DeadlineFailureDetector"
-
- # How often keep-alive heartbeat messages should be sent to each connection.
- heartbeat-interval = 4 s
-
- # Number of potentially lost/delayed heartbeats that will be
- # accepted before considering it to be an anomaly.
- # A margin to the `heartbeat-interval` is important to be able to survive sudden,
- # occasional, pauses in heartbeat arrivals, due to for example garbage collect or
- # network drop.
- acceptable-heartbeat-pause = 16 s
- }
-
- # Settings for the Phi accrual failure detector (http://www.jaist.ac.jp/~defago/files/pdf/IS_RR_2004_010.pdf
- # [Hayashibara et al]) used for remote death watch.
- # The default PhiAccrualFailureDetector will trigger if there are no heartbeats within
- # the duration heartbeat-interval + acceptable-heartbeat-pause + threshold_adjustment,
- # i.e. around 12.5 seconds with default settings.
- watch-failure-detector {
-
- # FQCN of the failure detector implementation.
- # It must implement akka.remote.FailureDetector and have
- # a public constructor with a com.typesafe.config.Config and
- # akka.actor.EventStream parameter.
- implementation-class = "akka.remote.PhiAccrualFailureDetector"
-
- # How often keep-alive heartbeat messages should be sent to each connection.
- heartbeat-interval = 1 s
-
- # Defines the failure detector threshold.
- # A low threshold is prone to generate many wrong suspicions but ensures
- # a quick detection in the event of a real crash. Conversely, a high
- # threshold generates fewer mistakes but needs more time to detect
- # actual crashes.
- threshold = 10.0
-
- # Number of the samples of inter-heartbeat arrival times to adaptively
- # calculate the failure timeout for connections.
- max-sample-size = 200
-
- # Minimum standard deviation to use for the normal distribution in
- # AccrualFailureDetector. Too low standard deviation might result in
- # too much sensitivity for sudden, but normal, deviations in heartbeat
- # inter arrival times.
- min-std-deviation = 100 ms
-
- # Number of potentially lost/delayed heartbeats that will be
- # accepted before considering it to be an anomaly.
- # This margin is important to be able to survive sudden, occasional,
- # pauses in heartbeat arrivals, due to for example garbage collect or
- # network drop.
- acceptable-heartbeat-pause = 10 s
-
-
- # How often to check for nodes marked as unreachable by the failure
- # detector
- unreachable-nodes-reaper-interval = 1s
-
- # After the heartbeat request has been sent the first failure detection
- # will start after this period, even though no heartbeat mesage has
- # been received.
- expected-response-after = 1 s
-
- }
-
- # After failed to establish an outbound connection, the remoting will mark the
- # address as failed. This configuration option controls how much time should
- # be elapsed before reattempting a new connection. While the address is
- # gated, all messages sent to the address are delivered to dead-letters.
- # Since this setting limits the rate of reconnects setting it to a
- # very short interval (i.e. less than a second) may result in a storm of
- # reconnect attempts.
- retry-gate-closed-for = 5 s
-
- # After catastrophic communication failures that result in the loss of system
- # messages or after the remote DeathWatch triggers the remote system gets
- # quarantined to prevent inconsistent behavior.
- # This setting controls how long the Quarantine marker will be kept around
- # before being removed to avoid long-term memory leaks.
- # WARNING: DO NOT change this to a small value to re-enable communication with
- # quarantined nodes. Such feature is not supported and any behavior between
- # the affected systems after lifting the quarantine is undefined.
- prune-quarantine-marker-after = 5 d
-
- # If system messages have been exchanged between two systems (i.e. remote death
- # watch or remote deployment has been used) a remote system will be marked as
- # quarantined after the two system has no active association, and no
- # communication happens during the time configured here.
- # The only purpose of this setting is to avoid storing system message redelivery
- # data (sequence number state, etc.) for an undefined amount of time leading to long
- # term memory leak. Instead, if a system has been gone for this period,
- # or more exactly
- # - there is no association between the two systems (TCP connection, if TCP transport is used)
- # - neither side has been attempting to communicate with the other
- # - there are no pending system messages to deliver
- # for the amount of time configured here, the remote system will be quarantined and all state
- # associated with it will be dropped.
- quarantine-after-silence = 5 d
-
- # This setting defines the maximum number of unacknowledged system messages
- # allowed for a remote system. If this limit is reached the remote system is
- # declared to be dead and its UID marked as tainted.
- system-message-buffer-size = 20000
-
- # This setting defines the maximum idle time after an individual
- # acknowledgement for system messages is sent. System message delivery
- # is guaranteed by explicit acknowledgement messages. These acks are
- # piggybacked on ordinary traffic messages. If no traffic is detected
- # during the time period configured here, the remoting will send out
- # an individual ack.
- system-message-ack-piggyback-timeout = 0.3 s
-
- # This setting defines the time after internal management signals
- # between actors (used for DeathWatch and supervision) that have not been
- # explicitly acknowledged or negatively acknowledged are resent.
- # Messages that were negatively acknowledged are always immediately
- # resent.
- resend-interval = 2 s
-
- # Maximum number of unacknowledged system messages that will be resent
- # each 'resend-interval'. If you watch many (> 1000) remote actors you can
- # increase this value to for example 600, but a too large limit (e.g. 10000)
- # may flood the connection and might cause false failure detection to trigger.
- # Test such a configuration by watching all actors at the same time and stop
- # all watched actors at the same time.
- resend-limit = 200
-
- # WARNING: this setting should not be not changed unless all of its consequences
- # are properly understood which assumes experience with remoting internals
- # or expert advice.
- # This setting defines the time after redelivery attempts of internal management
- # signals are stopped to a remote system that has been not confirmed to be alive by
- # this system before.
- initial-system-message-delivery-timeout = 3 m
-
- ### Transports and adapters
-
- # List of the transport drivers that will be loaded by the remoting.
- # A list of fully qualified config paths must be provided where
- # the given configuration path contains a transport-class key
- # pointing to an implementation class of the Transport interface.
- # If multiple transports are provided, the address of the first
- # one will be used as a default address.
- enabled-transports = ["akka.remote.netty.tcp"]
-
- # Transport drivers can be augmented with adapters by adding their
- # name to the applied-adapters setting in the configuration of a
- # transport. The available adapters should be configured in this
- # section by providing a name, and the fully qualified name of
- # their corresponding implementation. The class given here
- # must implement akka.akka.remote.transport.TransportAdapterProvider
- # and have public constructor without parameters.
- adapters {
- gremlin = "akka.remote.transport.FailureInjectorProvider"
- trttl = "akka.remote.transport.ThrottlerProvider"
- }
-
- ### Default configuration for the Netty based transport drivers
-
- netty.tcp {
- # The class given here must implement the akka.remote.transport.Transport
- # interface and offer a public constructor which takes two arguments:
- # 1) akka.actor.ExtendedActorSystem
- # 2) com.typesafe.config.Config
- transport-class = "akka.remote.transport.netty.NettyTransport"
-
- # Transport drivers can be augmented with adapters by adding their
- # name to the applied-adapters list. The last adapter in the
- # list is the adapter immediately above the driver, while
- # the first one is the top of the stack below the standard
- # Akka protocol
- applied-adapters = []
-
- transport-protocol = tcp
-
- # The default remote server port clients should connect to.
- # Default is 2552 (AKKA), use 0 if you want a random available port
- # This port needs to be unique for each actor system on the same machine.
- port = 2552
-
- # The hostname or ip clients should connect to.
- # InetAddress.getLocalHost.getHostAddress is used if empty
- hostname = ""
-
- # Use this setting to bind a network interface to a different port
- # than remoting protocol expects messages at. This may be used
- # when running akka nodes in a separated networks (under NATs or docker containers).
- # Use 0 if you want a random available port. Examples:
- #
- # akka.remote.netty.tcp.port = 2552
- # akka.remote.netty.tcp.bind-port = 2553
- # Network interface will be bound to the 2553 port, but remoting protocol will
- # expect messages sent to port 2552.
- #
- # akka.remote.netty.tcp.port = 0
- # akka.remote.netty.tcp.bind-port = 0
- # Network interface will be bound to a random port, and remoting protocol will
- # expect messages sent to the bound port.
- #
- # akka.remote.netty.tcp.port = 2552
- # akka.remote.netty.tcp.bind-port = 0
- # Network interface will be bound to a random port, but remoting protocol will
- # expect messages sent to port 2552.
- #
- # akka.remote.netty.tcp.port = 0
- # akka.remote.netty.tcp.bind-port = 2553
- # Network interface will be bound to the 2553 port, and remoting protocol will
- # expect messages sent to the bound port.
- #
- # akka.remote.netty.tcp.port = 2552
- # akka.remote.netty.tcp.bind-port = ""
- # Network interface will be bound to the 2552 port, and remoting protocol will
- # expect messages sent to the bound port.
- #
- # akka.remote.netty.tcp.port if empty
- bind-port = ""
-
- # Use this setting to bind a network interface to a different hostname or ip
- # than remoting protocol expects messages at.
- # Use "0.0.0.0" to bind to all interfaces.
- # akka.remote.netty.tcp.hostname if empty
- bind-hostname = ""
-
- # Enables SSL support on this transport
- enable-ssl = false
-
- # Sets the connectTimeoutMillis of all outbound connections,
- # i.e. how long a connect may take until it is timed out
- connection-timeout = 15 s
-
- # If set to "" then the specified dispatcher
- # will be used to accept inbound connections, and perform IO. If "" then
- # dedicated threads will be used.
- # Please note that the Netty driver only uses this configuration and does
- # not read the "akka.remote.use-dispatcher" entry. Instead it has to be
- # configured manually to point to the same dispatcher if needed.
- use-dispatcher-for-io = ""
-
- # Sets the high water mark for the in and outbound sockets,
- # set to 0b for platform default
- write-buffer-high-water-mark = 0b
-
- # Sets the low water mark for the in and outbound sockets,
- # set to 0b for platform default
- write-buffer-low-water-mark = 0b
-
- # Sets the send buffer size of the Sockets,
- # set to 0b for platform default
- send-buffer-size = 256000b
-
- # Sets the receive buffer size of the Sockets,
- # set to 0b for platform default
- receive-buffer-size = 256000b
-
- # Maximum message size the transport will accept, but at least
- # 32000 bytes.
- # Please note that UDP does not support arbitrary large datagrams,
- # so this setting has to be chosen carefully when using UDP.
- # Both send-buffer-size and receive-buffer-size settings has to
- # be adjusted to be able to buffer messages of maximum size.
- maximum-frame-size = 128000b
-
- # Sets the size of the connection backlog
- backlog = 4096
-
- # Enables the TCP_NODELAY flag, i.e. disables Nagle’s algorithm
- tcp-nodelay = on
-
- # Enables TCP Keepalive, subject to the O/S kernel’s configuration
- tcp-keepalive = on
-
- # Enables SO_REUSEADDR, which determines when an ActorSystem can open
- # the specified listen port (the meaning differs between *nix and Windows)
- # Valid values are "on", "off" and "off-for-windows"
- # due to the following Windows bug: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4476378
- # "off-for-windows" of course means that it's "on" for all other platforms
- tcp-reuse-addr = off-for-windows
-
- # Used to configure the number of I/O worker threads on server sockets
- server-socket-worker-pool {
- # Min number of threads to cap factor-based number to
- pool-size-min = 2
-
- # The pool size factor is used to determine thread pool size
- # using the following formula: ceil(available processors * factor).
- # Resulting size is then bounded by the pool-size-min and
- # pool-size-max values.
- pool-size-factor = 1.0
-
- # Max number of threads to cap factor-based number to
- pool-size-max = 2
- }
-
- # Used to configure the number of I/O worker threads on client sockets
- client-socket-worker-pool {
- # Min number of threads to cap factor-based number to
- pool-size-min = 2
-
- # The pool size factor is used to determine thread pool size
- # using the following formula: ceil(available processors * factor).
- # Resulting size is then bounded by the pool-size-min and
- # pool-size-max values.
- pool-size-factor = 1.0
-
- # Max number of threads to cap factor-based number to
- pool-size-max = 2
- }
-
-
- }
-
- netty.udp = ${akka.remote.netty.tcp}
- netty.udp {
- transport-protocol = udp
- }
-
- netty.ssl = ${akka.remote.netty.tcp}
- netty.ssl = {
- # Enable SSL/TLS encryption.
- # This must be enabled on both the client and server to work.
- enable-ssl = true
-
- security {
- # This is the Java Key Store used by the server connection
- key-store = "keystore"
-
- # This password is used for decrypting the key store
- key-store-password = "changeme"
-
- # This password is used for decrypting the key
- key-password = "changeme"
-
- # This is the Java Key Store used by the client connection
- trust-store = "truststore"
-
- # This password is used for decrypting the trust store
- trust-store-password = "changeme"
-
- # Protocol to use for SSL encryption, choose from:
- # Java 6 & 7:
- # 'SSLv3', 'TLSv1'
- # Java 7:
- # 'TLSv1.1', 'TLSv1.2'
- protocol = "TLSv1"
-
- # Example: ["TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA"]
- # You need to install the JCE Unlimited Strength Jurisdiction Policy
- # Files to use AES 256.
- # More info here:
- # http://docs.oracle.com/javase/7/docs/technotes/guides/security/SunProviders.html#SunJCEProvider
- enabled-algorithms = ["TLS_RSA_WITH_AES_128_CBC_SHA"]
-
- # There are three options, in increasing order of security:
- # "" or SecureRandom => (default)
- # "SHA1PRNG" => Can be slow because of blocking issues on Linux
- # "AES128CounterSecureRNG" => fastest startup and based on AES encryption
- # algorithm
- # "AES256CounterSecureRNG"
- #
- # The following are deprecated in Akka 2.4. They use one of 3 possible
- # seed sources, depending on availability: /dev/random, random.org and
- # SecureRandom (provided by Java)
- # "AES128CounterInetRNG"
- # "AES256CounterInetRNG" (Install JCE Unlimited Strength Jurisdiction
- # Policy Files first)
- # Setting a value here may require you to supply the appropriate cipher
- # suite (see enabled-algorithms section above)
- random-number-generator = ""
- }
- }
-
- ### Default configuration for the failure injector transport adapter
-
- gremlin {
- # Enable debug logging of the failure injector transport adapter
- debug = off
- }
-
- ### Default dispatcher for the remoting subsystem
-
- default-remote-dispatcher {
- type = Dispatcher
- executor = "fork-join-executor"
- fork-join-executor {
- # Min number of threads to cap factor-based parallelism number to
- parallelism-min = 2
- parallelism-max = 2
- }
- }
-
- backoff-remote-dispatcher {
- type = Dispatcher
- executor = "fork-join-executor"
- fork-join-executor {
- # Min number of threads to cap factor-based parallelism number to
- parallelism-min = 2
- parallelism-max = 2
- }
- }
-
-
- }
-
- }
akka-testkit
- ######################################
- # Akka Testkit Reference Config File #
- ######################################
-
- # This is the reference config file that contains all the default settings.
- # Make your edits/overrides in your application.conf.
-
- akka {
- test {
- # factor by which to scale timeouts during tests, e.g. to account for shared
- # build system load
- timefactor = 1.0
-
- # duration of EventFilter.intercept waits after the block is finished until
- # all required messages are received
- filter-leeway = 3s
-
- # duration to wait in expectMsg and friends outside of within() block
- # by default
- single-expect-default = 3s
-
- # The timeout that is added as an implicit by DefaultTimeout trait
- default-timeout = 5s
-
- calling-thread-dispatcher {
- type = akka.testkit.CallingThreadDispatcherConfigurator
- }
- }
- }
akka-cluster-metrics
~~~~~~~~~~~~--------
akka-cluster-tools
~~~~~~~~~~~~------
- ############################################
- # Akka Cluster Tools Reference Config File #
- ############################################
-
- # This is the reference config file that contains all the default settings.
- # Make your edits/overrides in your application.conf.
-
- # //#pub-sub-ext-config
- # Settings for the DistributedPubSub extension
- akka.cluster.pub-sub {
- # Actor name of the mediator actor, /system/distributedPubSubMediator
- name = distributedPubSubMediator
-
- # Start the mediator on members tagged with this role.
- # All members are used if undefined or empty.
- role = ""
-
- # The routing logic to use for 'Send'
- # Possible values: random, round-robin, broadcast
- routing-logic = random
-
- # How often the DistributedPubSubMediator should send out gossip information
- gossip-interval = 1s
-
- # Removed entries are pruned after this duration
- removed-time-to-live = 120s
-
- # Maximum number of elements to transfer in one message when synchronizing the registries.
- # Next chunk will be transferred in next round of gossip.
- max-delta-elements = 3000
-
- # The id of the dispatcher to use for DistributedPubSubMediator actors.
- # If not specified default dispatcher is used.
- # If specified you need to define the settings of the actual dispatcher.
- use-dispatcher = ""
-
- }
- # //#pub-sub-ext-config
-
- # Protobuf serializer for cluster DistributedPubSubMeditor messages
- akka.actor {
- serializers {
- akka-pubsub = "akka.cluster.pubsub.protobuf.DistributedPubSubMessageSerializer"
- }
- serialization-bindings {
- "akka.cluster.pubsub.DistributedPubSubMessage" = akka-pubsub
- }
- serialization-identifiers {
- "akka.cluster.pubsub.protobuf.DistributedPubSubMessageSerializer" = 9
- }
- }
-
-
- # //#receptionist-ext-config
- # Settings for the ClusterClientReceptionist extension
- akka.cluster.client.receptionist {
- # Actor name of the ClusterReceptionist actor, /system/receptionist
- name = receptionist
-
- # Start the receptionist on members tagged with this role.
- # All members are used if undefined or empty.
- role = ""
-
- # The receptionist will send this number of contact points to the client
- number-of-contacts = 3
-
- # The actor that tunnel response messages to the client will be stopped
- # after this time of inactivity.
- response-tunnel-receive-timeout = 30s
-
- # The id of the dispatcher to use for ClusterReceptionist actors.
- # If not specified default dispatcher is used.
- # If specified you need to define the settings of the actual dispatcher.
- use-dispatcher = ""
- }
- # //#receptionist-ext-config
-
- # //#cluster-client-config
- # Settings for the ClusterClient
- akka.cluster.client {
- # Actor paths of the ClusterReceptionist actors on the servers (cluster nodes)
- # that the client will try to contact initially. It is mandatory to specify
- # at least one initial contact.
- # Comma separated full actor paths defined by a string on the form of
- # "akka.tcp://system@hostname:port/system/receptionist"
- initial-contacts = []
-
- # Interval at which the client retries to establish contact with one of
- # ClusterReceptionist on the servers (cluster nodes)
- establishing-get-contacts-interval = 3s
-
- # Interval at which the client will ask the ClusterReceptionist for
- # new contact points to be used for next reconnect.
- refresh-contacts-interval = 60s
-
- # How often failure detection heartbeat messages should be sent
- heartbeat-interval = 2s
-
- # Number of potentially lost/delayed heartbeats that will be
- # accepted before considering it to be an anomaly.
- # The ClusterClient is using the akka.remote.DeadlineFailureDetector, which
- # will trigger if there are no heartbeats within the duration
- # heartbeat-interval + acceptable-heartbeat-pause, i.e. 15 seconds with
- # the default settings.
- acceptable-heartbeat-pause = 13s
-
- # If connection to the receptionist is not established the client will buffer
- # this number of messages and deliver them the connection is established.
- # When the buffer is full old messages will be dropped when new messages are sent
- # via the client. Use 0 to disable buffering, i.e. messages will be dropped
- # immediately if the location of the singleton is unknown.
- # Maximum allowed buffer size is 10000.
- buffer-size = 1000
- }
- # //#cluster-client-config
-
- # Protobuf serializer for ClusterClient messages
- akka.actor {
- serializers {
- akka-cluster-client = "akka.cluster.client.protobuf.ClusterClientMessageSerializer"
- }
- serialization-bindings {
- "akka.cluster.client.ClusterClientMessage" = akka-cluster-client
- }
- serialization-identifiers {
- "akka.cluster.client.protobuf.ClusterClientMessageSerializer" = 15
- }
- }
-
- # //#singleton-config
- akka.cluster.singleton {
- # The actor name of the child singleton actor.
- singleton-name = "singleton"
-
- # Singleton among the nodes tagged with specified role.
- # If the role is not specified it's a singleton among all nodes in the cluster.
- role = ""
-
- # When a node is becoming oldest it sends hand-over request to previous oldest,
- # that might be leaving the cluster. This is retried with this interval until
- # the previous oldest confirms that the hand over has started or the previous
- # oldest member is removed from the cluster (+ akka.cluster.down-removal-margin).
- hand-over-retry-interval = 1s
-
- # The number of retries are derived from hand-over-retry-interval and
- # akka.cluster.down-removal-margin (or ClusterSingletonManagerSettings.removalMargin),
- # but it will never be less than this property.
- min-number-of-hand-over-retries = 10
- }
- # //#singleton-config
-
- # //#singleton-proxy-config
- akka.cluster.singleton-proxy {
- # The actor name of the singleton actor that is started by the ClusterSingletonManager
- singleton-name = ${akka.cluster.singleton.singleton-name}
-
- # The role of the cluster nodes where the singleton can be deployed.
- # If the role is not specified then any node will do.
- role = ""
-
- # Interval at which the proxy will try to resolve the singleton instance.
- singleton-identification-interval = 1s
-
- # If the location of the singleton is unknown the proxy will buffer this
- # number of messages and deliver them when the singleton is identified.
- # When the buffer is full old messages will be dropped when new messages are
- # sent via the proxy.
- # Use 0 to disable buffering, i.e. messages will be dropped immediately if
- # the location of the singleton is unknown.
- # Maximum allowed buffer size is 10000.
- buffer-size = 1000
- }
- # //#singleton-proxy-config
-
- # Serializer for cluster ClusterSingleton messages
- akka.actor {
- serializers {
- akka-singleton = "akka.cluster.singleton.protobuf.ClusterSingletonMessageSerializer"
- }
- serialization-bindings {
- "akka.cluster.singleton.ClusterSingletonMessage" = akka-singleton
- }
- serialization-identifiers {
- "akka.cluster.singleton.protobuf.ClusterSingletonMessageSerializer" = 14
- }
- }
akka-cluster-sharding
~~~~~~~~~~~~---------
akka-distributed-data
~~~~~~~~~~~~---------
- ##############################################
- # Akka Distributed DataReference Config File #
- ##############################################
-
- # This is the reference config file that contains all the default settings.
- # Make your edits/overrides in your application.conf.
-
-
- #//#distributed-data
- # Settings for the DistributedData extension
- akka.cluster.distributed-data {
- # Actor name of the Replicator actor, /system/ddataReplicator
- name = ddataReplicator
-
- # Replicas are running on members tagged with this role.
- # All members are used if undefined or empty.
- role = ""
-
- # How often the Replicator should send out gossip information
- gossip-interval = 2 s
-
- # How often the subscribers will be notified of changes, if any
- notify-subscribers-interval = 500 ms
-
- # Maximum number of entries to transfer in one gossip message when synchronizing
- # the replicas. Next chunk will be transferred in next round of gossip.
- max-delta-elements = 1000
-
- # The id of the dispatcher to use for Replicator actors. If not specified
- # default dispatcher is used.
- # If specified you need to define the settings of the actual dispatcher.
- use-dispatcher = ""
-
- # How often the Replicator checks for pruning of data associated with
- # removed cluster nodes.
- pruning-interval = 30 s
-
- # How long time it takes (worst case) to spread the data to all other replica nodes.
- # This is used when initiating and completing the pruning process of data associated
- # with removed cluster nodes. The time measurement is stopped when any replica is
- # unreachable, so it should be configured to worst case in a healthy cluster.
- max-pruning-dissemination = 60 s
-
- # Serialized Write and Read messages are cached when they are sent to
- # several nodes. If no further activity they are removed from the cache
- # after this duration.
- serializer-cache-time-to-live = 10s
-
- }
- #//#distributed-data
-
- # Protobuf serializer for cluster DistributedData messages
- akka.actor {
- serializers {
- akka-data-replication = "akka.cluster.ddata.protobuf.ReplicatorMessageSerializer"
- akka-replicated-data = "akka.cluster.ddata.protobuf.ReplicatedDataSerializer"
- }
- serialization-bindings {
- "akka.cluster.ddata.Replicator$ReplicatorMessage" = akka-data-replication
- "akka.cluster.ddata.ReplicatedDataSerialization" = akka-replicated-data
- }
- serialization-identifiers {
- "akka.cluster.ddata.protobuf.ReplicatedDataSerializer" = 11
- "akka.cluster.ddata.protobuf.ReplicatorMessageSerializer" = 12
- }
- }
Актори
Модель акторів провадить вищий рівень абстракції для написання конкурентних та розподілених систем. Він вивільнює розробника від явного керування блокуваннями та потоками, спрощуючи написання конкурентних та паралельних систем. Актори були визначені в 1973 році в документі Карла Хьювіта, але стали популярними тільки з мовою Erlang, та з виликим успіхом використовувались як приклад на Ericsson для побудови висококонкурентних та надійних систем телекома.
API акторів Akka подібне до акторів Scala, що були позичені в частині синтаксису з Erlang.
Створення акторів
Визначення класу актора
Актори реалізуються поширенням базового трейту Actor, та реалізацією методаreceive. Метод receiveповинен визначати серію перевірок case (що має тип PartialFunction[Any, Unit]), що визначає, які повідомлення може обробляти цей актор, використовуючи стандартне співпадіння шаблонів Scala, разом з тим, як ці повідомлення мають бути оброблені.
Ось приклад:
- import akka.actor.Actor
- import akka.actor.Props
- import akka.event.Logging
-
- class MyActor extends Actor {
- val log = Logging(context.system, this)
-
- def receive = {
- case "test" => log.info("received test")
- case _ => log.info("received unknown message")
- }
- }
Будь ласка, зауважте, що цикл повідомлень Akka receive є вичерпним, що має відмінність до Erlang, та останніми Scala Actors. Це означає, що вам треба провадити співпадіння шаблонів для всіх повідомлень, що ви можете сприймати, якщо ви бажаєте бути в змозі обробляти невідомі повідомлення, та коли вам треба мати випадок по замовчанню, як в прикладі вище. В іншому випадку буде опублікований akka.actor.UnhandledMessage(message, sender, recipient) в ActorSystem'sEventStream.
Надалі занотуйте, що тип результата поведінки, визначеної вище, є Unit; якщо актор буде відповідати на надіслане повідомлення, це має бути зроблене явно, як показане нижче.
Результат метода receive є об'єкт часткової функції, що зберігається з актором як “внутрішня поведінка”, дивіться Become/Unbecome для подальшої інформації щодо зміни поведінки актора після створення.
Props
Props є клас конфігурації, щоб задати опції для створення акторів, думайте про це як про незмінний, і, таким чином, розподілений рецепт для створення актора, включаючи асоційовану інформацію по розгортанню (тобто, який диспечер використовується, дивіться нижче). Ось деякі приклади того, як створити примірник Props.
- import akka.actor.Props
-
- val props1 = Props[MyActor]
- val props2 = Props(new ActorWithArgs("arg")) // уважно, дивіться нижче
- val props3 = Props(classOf[ActorWithArgs], "arg")
Другий варіант показує, як передавати аргументи конструктора до створюваного Actor, але це треба використовувати тільки за межами акторів, як пояснено нижче.
Останній рядок показує можливість передавати аргументи конструктора, безвідносно до контексту, де він використовується. Присутність відповідного конструктора перевіряється під час створення об'єктаProps, що призводить до IllegalArgumentException, якщо конструктор не буде знайдений, або буде знайдено більше одного.
Небезпечні варіанти
- // НЕ РЕКОМЕНДОВАНЕ в іншому акторі:
- // заохочує замикання замикаючого класа
- val props7 = Props(new MyActor)
Цей метод не рекомендований для використання в іншому акторі, бо він заохочує на оточуєму полі зору, що призводить до несеріалізуємого Props та можливого стану гонок (що руйнує енкапсуляцію актора). Ми будемо провадити базоване на макро рішення в подальшому релізі, що дозволить подібний синтаксис без головняка, та на той час попереднє рішення піде у відставку. З іншого боку, використання цього варіанту в фабриці Props в об'єкті-компанйоні актора, як документовано в “Рекомендованих практиках” нижче, є досить прийнятним.
Існує два випадка використання для ціх методів: передання аргументів конструктора до актора — що вирішене недавно введеним методом Props.apply(clazz, args)вище, або рекомендована практика нижче — та створення акторів “по місцю” як анонімних класів. Останнє має бути вирішене, роблячи ці актори іменованими класами (якщо вони не декларовані на object вищого рівня, коли замикаючий примірник посиланняthis треба передати як перший аргумент).
Попередження
Визначення одного актора в іншому є дуже небезпечним, та руйнує інкапсуляцію акторів. Ніколи не передавайте посилання актора this до Props!
Рекомендовані практики
Є гарною ідеєю запровадити методи-фабрики на об'єкті-компанйоні кожного Actor, що допоможе утримувати створення відповідних Props так близько до визначення актора, як це можливо. Це також уникає пасток, асоціайованих з використанням метода Props.apply(...), що приймає аргумент по-імені, оскільки в об'єкті-компанйоні наданий блок кода не буде залишати посилання на оточуюче поле зору:
- object DemoActor {
- /**
- * Створити Props для актора цього типу.
- *
- * @param magicNumber Магічне число, що передається до конструктора актора.
- * @return Props для створення цього актора, що може потім бути
- * доконфігуроване (викликом на ньому `.withDispatcher()`)
- */
- def props(magicNumber: Int): Props = Props(new DemoActor(magicNumber))
- }
-
- class DemoActor(magicNumber: Int) extends Actor {
- def receive = {
- case x: Int => sender() ! (x + magicNumber)
- }
- }
-
- class SomeOtherActor extends Actor {
- // Props(new DemoActor(42)) не буде безпечним
- context.actorOf(DemoActor.props(42), "demo")
- // ...
- }
Інша гарна практика є декларування того, які повідомлення Actor може отримувати, в об'єкті-компанйоні Actor, щоб спростити розуміння того, що можна очікувати:
- object MyActor {
- case class Greeting(from: String)
- case object Goodbye
- }
- class MyActor extends Actor with ActorLogging {
- import MyActor._
- def receive = {
- case Greeting(greeter) => log.info(s"Мене привітав $greeter.")
- case Goodbye => log.info("Хтось мені сказав до побачення.")
- }
- }
Створення акторів з Props
Актори можуть бути створені з передачею примірника Props до метода-фабрики actorOf, що доступне наonActorSystem та ActorContext.
- import akka.actor.ActorSystem
-
- // ActorSystem є важким об'єктом: створюйте тільки один на застосування
- val system = ActorSystem("mySystem")
- val myActor = system.actorOf(Props[MyActor], "myactor2")
Використання ActorSystem буде створювати акторів вищого рівня, за якими наглядає актор-захисник, наданий смстемою акторів, тоді як використання контексту акторів створить актора-дитинча.
- class FirstActor extends Actor {
- val child = context.actorOf(Props[MyActor], name = "myChild")
- // деяка поведінка ...
- }
Рекомендовано створювати ієрархію дітей, дітей дітей, і так далі, щоб це пасувало до логічної структури обробки збоїв застосування, також дивіться Системи акторів.
Виклик до actorOf повертає примірник ActorRef. Це посилання на примірник актора, та є єдиним способом взаємодіяти з ним. ActorRef є незмінним, та має відношення один до одного з акторами, які він представляє. ActorRef також серіалізується та є обізнаним з мережею. Це означає, що ви можете серіалізувати його, надіслати по дроті, та використати на віддаленому вузлі, та він буде представляти той самий Actor на оригінальному вузлі, навіть по мережі.
Іменований параметр є опціональним, але ви маєте переважно іменувати ваших акторів, оскільки це використовується в повідомленнях журналу для ідентифікації акторів. Ім'я мусить не бути порожнім, або починатись з $, але воно може вістити URL-закодовані символи (наприклад, %20для проміжку). Якщо надане ім'я вже використовується іншим актором-дитям того ж батька, викликаєтьсяInvalidActorNameException.
Актори автоматично асинхронно стартують після створення.
Впровадження залежностей (DI)
Якщо ваш актор має конструктор, що приймає параметри, тоді вони мають бути також і частиною Props, як описане вище. Аде є випадки, коли має використовуватись метод-фабрика, наприклад, коли справжні аргументи конструктора визначаються через фреймворк впровадження залежностей.
- import akka.actor.IndirectActorProducer
-
- class DependencyInjector(applicationContext: AnyRef, beanName: String)
- extends IndirectActorProducer {
-
- override def actorClass = classOf[Actor]
- override def produce =
- // отримати свіжий примірник Actor з фреймворку DI ...
- }
-
- val actorRef = system.actorOf(
- Props(classOf[DependencyInjector], applicationContext, "hello"),
- "helloBean")
Попередження
Ви можете мати спокусу часом запропонувати IndirectActorProducer, що завжди повертає той же примірник, тобто використовувати lazy val. Це не підтримується, бо це іде всупереч ідеї рестарта актора, що описане тут: Що значить рестарт.
Коли використовується фреймворк впровадження залежностей, біни акторів НЕ ПОВИННІ мати оглядовість синглтона.
Технології впровадження залежностей та інтеграція з фреймворками впровадження залежностей більш глибоко описані в інструкції Використанні Akka з впровадженням залежностей, та туторіалі Akka Java Spring в Lightbend Activator.
Поштова скринька Входящі
Коли ви пишете код за межами акторів, що буде комунікувати з акторами, шаблонask може бути одним з рішень (дивіться нижче), але ж дві речі, що він не може зробити: відіслати декілька відповідей (тобто, підписатиActorRef на сервіс повідомлень), та слідкувати за життєвим циклом іншого актора. Для цього існує класInbox:
- implicit val i = inbox()
- echo ! "hello"
- i.receive() should ===("hello")
Є неявне перетворення з поштової скриньки на посилання до актора, що означає, що в цьому прикладі посилання надсилача буде приховане в поштовій скринці актора. Це дозволяє відповісти в останньому рядку. Нагляд за актором також простий:
- val target = // деякий актор
- val i = inbox()
- i watch target
Actor API
Трейт Actor визначає тільки один абстрактний метод, вже вказаний receive, що реалізує поведінку актора.
Якщо поточна поведінка актора не співпадає з отрисанним повідомленням, викликається unhandled, що по замовчанню публікує на потоці системи акторів akka.actor.UnhandledMessage(message, sender, recipient) (встановіть елемент конфігурації akka.actor.debug.unhandled в on, щоб перетворювати їх на справжні повідомлення Debug).
На додаток це дає:
self посилання до ActorRef актора
sender посилання на актора-надсилача останнього отриманого повідомлення, зазвичай використовується як описане в Відповідь на повідомлення
supervisorStrategy перезаписуване користувачем визначення стратегії, що буде застосовуватись для нагляду за акторами-дітьми
Ця стратегія звичайно визначається в акторі, щоб мати доступ до внутрішнього стану актора в функції прийняття рішення: оскільки відмова комунікується як повідомлення, надіслане супервізору, та обробляється як любі інші повідомлення (хоча і за межами звичайної поведінки), всі значення та змінні в акторі є доступними, як і посиланняsender (що буде безпосереднім дитям, що сповіщає про збій; якщо збій відбувся в більш віддаленому нащадку, він буде все одне повідомлятись на один рівень за раз).
context викриває контекстуальну інформацію для актора, та поточне повідомлення, таким чином:
- методи-фабрики для створення акторів (
actorOf) - система, що якої належить актор
- батьківський наглядач
- дитя, що наглядається
- моніторинг життєвого циклу
- гарача заміна стеку поведінки, як описане в Become/Unbecome
Ви можете імпортувати члени в context, щоб уникнути префіксів доступу доcontext.
- class FirstActor extends Actor {
- import context._
- val myActor = actorOf(Props[MyActor], name = "myactor")
- def receive = {
- case x => myActor ! x
- }
- }
Інші видимі методи є переписувані користувачем перехоплення життєвого циклу, що описуються наступним чином:
- def preStart(): Unit = ()
-
- def postStop(): Unit = ()
-
- def preRestart(reason: Throwable, message: Option[Any]): Unit = {
- context.children foreach { child ⇒
- context.unwatch(child)
- context.stop(child)
- }
- postStop()
- }
-
- def postRestart(reason: Throwable): Unit = {
- preStart()
- }
Показана вище реалізація по замовчанню провадиться трейтом Actor.
Життєвий цикл актора

Шлях в системі акторів представляє "місце ", що може бути зайняте проживаючим актором. На початку (окремо від ініційованих системою акторів) шлях пустий. Коли викликається actorOf(), він присвоює інкарнацію актора, описаного в переданомуProps до вказаного шляха. Інкарнація актора ідентифікується по шляху та UID. Рестарт тільки замінює примірник Actor, визначений в Props, але інкарнація, та, таким чином UID, залишаються тими самими.
Життєвий цикл інкарнації завершується, коли актор зупиняється. В цій точці викликаються відповідні події життєвого цикла, та наглядаючий актор повідомляється про завершення. Після того, як інкарнація зупинилась, шлях може бути знову використаний для створення актора за допомогою actorOf(). В цьому випадку ім'я нової інкарнації буде таким самим, що і в попередньої, але UID буде відрізнятись. Актор може бути зупинений самим актором, іншим актором, абоActorSystem (дивіться Зупинка акторів).
Зауваження
Важливо зауважити, що Actors не зупиняється автоматично, коли на нього немає посилань, кожний створений Actor має бути також явно знищений. Одне спрощення полягає в тому, що зупинення батьківського актора також рекурсивно зупиняє всі дитячі актори, що створив цей батько.
ActorRef завжди представляє інкарнацію (шлях та UID), не тільки даний шлях. Таким чином, якщо актор зупиняєтсья, та створюється новий з тим же ім'ям, ActorRef старої інкарнації не буде вказувати на нову.
ActorSelection, з іншого боку, вказує на шлях (або декілька шляхів, якщо вказана зірочка), та абсолютно не звертаючи увагу, на яку інкарнацію, що наразі займає цей шлях. ActorSelection не може відслідковуватись з декількох причин. Можливо розрішити ActorRef поточної інкарнації, що проживає за шляхом, надсилаючи повідомлення Identify доActorSelection, що поверне ActorIdentity, що міститиме коректне посилання (дивіться Ідентифікація акторів через Actor Selection). Це також можна зробити за допомогою метода resolveOne на ActorSelection, що повертає Future співпадаючих ActorRef.
Моніторинг життєвого циклу, aka DeathWatch
Щоб отримувати повідомлення коли інший актор завершується (тобто, зупиняється назавжди, не тимчасово дає збій та рестартує), актор може зареєструвати себе для отримання повідомлення Terminated, що надсилається іншим актором під час завершення (дивіться Зупинка акторів). Ця послуга провадиться компонентом DeathWatch системи акторів.
Реєстрація монітора є простою:
- import akka.actor.{ Actor, Props, Terminated }
-
- class WatchActor extends Actor {
- val child = context.actorOf(Props.empty, "child")
- context.watch(child) // <-- це єдиний виклик, що треба для реєсрації
- var lastSender = context.system.deadLetters
-
- def receive = {
- case "kill" =>
- context.stop(child); lastSender = sender()
- case Terminated(`child`) => lastSender ! "finished"
- }
- }
Треба зауважити, що повідомлення Terminated генерується незалежно від порядку, в якому відбувається реєстрація та завершення. Зокрема, наглядаючий актор отримає повідомлення Terminated, навіть якщо нагляданий актор вже завершений під час реєстрації.
Реєстрація декілька разів не обов'язково призведе до генерації декількох повідомлень, але немає гарантії, що тільки рівно одне повідомлення буде отримане: якщо завершення нагляданого актора було згенероване та поставлене в чергу, та відбулась інша реєстрація перед отробкою повідомлення, тоді друге повідомлення буде поставлене в чергу, оскільки реєстрація для мониторинга вже завершеного актора призводить до безпосередньої генерації повідомлення Terminated.
Також можливо відреєструватись з нагляду життездатності іншого актора, викоистовуючи context.unwatch(target). Це робить, навіть якщо повідомлення Terminated вже було поставлене до черги в поштову скриньку; після виклику unwatch жодних повідомлень Terminated для цього актора більше не буде оброблене.
Перехоплювач старту
Зразу після запуску актора викликається його метод preStart.
- override def preStart() {
- child = context.actorOf(Props[MyActor], "child")
- }
Цей метод викликається, коли актор створюється перший раз. Під час рестарту він викликається реалізацією по замовчаннюpostRestart, що означає, що переписуючи цей метод ви можете обрати, чи код ініціалізації цього метода викликається тільки рівно один раз для цього актора, або кожний рестарт. Код ініціалізації, що є частиною конструктора актора, буде завжди викликатись, коли створюється примірник класа актора, що відбувається кожний рестарт.
Перехоплювач рестарту
Всі актори наглядаються, тобто приєднані до іншого актора зі стратегією обробки відмови. Актори можуть бути рестартовані, в випадку виникнення виключення під час обробки повідомлення (дивіться Нагляд та мониторинг). Цей рестарт включає наступні перелічені перехоплення:
Старий актор інформується викликом preRestart з виключенням, яке призвело до рестарту, та повідомленням, що викликало це виключення; останнє може бути None, якщо рестарт не викликаний обробкою повідомлення, тобто, коли супервізор не перехопив виключення, та він рестартує за бажанням свого супервізора, або якщо актор рестартує через відмову однорівневого актора. Якщо повідомлення доступне, тоді і надсилач цього повідомлення доступне в звичайний спосіб (тобто, через виклик sender).
Цей метод є кращим місцем для очистки, підготування почви для нового примірника актора, тощо. По замовчанню зупиняються всі діти, та викликається postStop.
Використовується початковий виклик з actorOf для створення нового примірника.
Викликається метод postRestart нового актора, з виключенням, що спричинило рестарт. По замовчанню викликається preStart, так само, як під час звичайного запуску.
Рестарт актора заміщує тільки дійсний примірник актора; вміст поштової скриньки не змінюється під час рестарту, так що обробка повідомлень буде відновлена після повернення з перехоплювача postRestart. Повідомлення, що спричинило виключення, не буде надіслане знову. Любе повідомлення, надіслане до актора під час рестарта, буде поставлене до черги в звичайний спосіб.
Попередження
Майте на увазі, що порядок повідомлень відмови відносно користувацьких повідомлень не визначений. Зокрема, батько може рестартувати свого дитя до того, як він обробить останнє повідомлення, надіслане дитиною до збою. Дивіться Дискусія: порядок повідомлень щодо деталей.
Перехоплювач зупинки
Після зупинки актора викликається його перехоплювач postStop, що може бути використаний, наприклад, для дереєстрації цього актора від інших сервісів. Цей перехоплювач гарантовано виконується після відключення постановки повідомлень в чергу для цього актора, тобто, повідомлення, надіслані до зупиненого актора будуть переправлені до deadLetters поточної ActorSystem.
Ідентифікація акторів через Actor Selection
Як описане в Посиланнях на акторів, хляхи та адреси, кожний актор має унікальний логиіний шлях, що отримується слідуючи ланцюжку від дитини до батька , доки не буде досягникий корінь системи акторів, та це має фізичний шлях, що може відрізнятись, якщо ланцюжок супервізорів включає будь які віддплені супервізори. Ці шляхи використовуються системою для пошуку акторів, тобто, коли прийняте віддалене повідомлення, та шукається отримувач. Але вони також корисні і більш прямолінійно: актори можуть шукати інших акторів, вказуючи абсолютні або відносні шляхи — логічні або фізичні — та отримати у відповідь ActorSelection з результатом:
- // буде шукати абсолютний шлях
- context.actorSelection("/user/serviceA/aggregator")
- // буде шукати брата в того ж предка-супервізора
- context.actorSelection("../joe")
Зауваження
Завжди краще комунікувати з іншим актором з використанням його ActorRef, замість покладатись на ActorSelection. Виключенням може бути:
- надсилання повідомлень з використанням можливості Доставки щонайменьше-раз
- ініціалізвція першого контакту з віддаленою системою
В усіх інших випадках ActorRefs може бути запроваджений під час створення або ініціалізації актора, передаючи їх від батька дітям, або вводячи акторів, надсилаючи їх ActorRefs іншим акторам в повідомленнях.
Наданий шлях розбирається як java.net.URI, що, в основному, означає, що він розбивається по / на елементи шляху. Якщо шлях починається з /, він є абсолютним, та пошук починається з кореневого охоронця (що є батьком "/user"); інакше він починається з поточного актора. Якщо елемент шляху дорівнює .., пошук зробить крок “догори”, в напрямку супервізора поточного актора, інакше він піде “донизу”, до вказаного дитя. Треба занотувати, що .. в шляхах акторів тут і надалі завжди означає логічну структуру, тобто, супервізор.
Елементи шляху селектора актора може містити узагальнуючі символи для широкомовлення повідомлень до цього розділу:
- // буде шукати всіх дітей serviceB з іменами, що починаються на worker
- context.actorSelection("/user/serviceB/worker*")
- // буде шукати всіх близнят того ж супервізора
- context.actorSelection("../*")
Повідомлення можуть бути надіслані через ActorSelection, та шлях ActorSelection шукаються при доставленні кожного повідомлення. Якщо селектор не співпадає з жодним актором, повідомлення будуть відкинуті.
Щоб отримати ActorRef для ActorSelection, вам потрібно надіслати повідомлення до виборки, та використовувати посилання sender() відповіді від актора. Існує вбудоване повідомлення Identify, що розуміють всі актори, та автоматично відповідають на нього повідомленням ActorIdentity, що містить ActorRef. Це повідомлення обробляється зустрічними акторами особливим чином, в тому сенсі, що якщо пошук конкретного імені схибить (тобто, елемент шляху без зірочок не відповідають живому актору), тоді генерується негативний результат. Будь ласка, занотуйте, що це не означає, що доставлення цієї відповіді гарантовано, це все ще звичайне повідомлення.
- import akka.actor.{ Actor, Props, Identify, ActorIdentity, Terminated }
-
- class Follower extends Actor {
- val identifyId = 1
- context.actorSelection("/user/another") ! Identify(identifyId)
-
- def receive = {
- case ActorIdentity(`identifyId`, Some(ref)) =>
- context.watch(ref)
- context.become(active(ref))
- case ActorIdentity(`identifyId`, None) => context.stop(self)
-
- }
-
- def active(another: ActorRef): Actor.Receive = {
- case Terminated(`another`) => context.stop(self)
- }
- }
Ви також можете захопити ActorRef для ActorSelection за допомогою метода resolveOne на ActorSelection. Він повертає Future співпавшого ActorRef, якщо такий автор існує. Він завершається збоєм [[akka.actor.ActorNotFound]], якщо такого актора не існує, або ідентифікація не завершилась в наданий час таймаута.
Віддалені адреси акторів також можуть переглядатись, якщо дозволений ремоутинг remoting:
- context.actorSelection("akka.tcp://app@otherhost:1234/user/serviceB")
Приклад, що демонструє пошук актора, наданий в Прикладі ремоутинга.
Повідомлення та незмінність
ВАЖЛИВО: Повідомлення можуть бути любим типом об'єктів, але мають бути незмінними. Scala не можу примусити до незмінності (поки що), так що це повинно бути зроблене за домовленостю. Примітиви, як String, Int, Boolean завжди є незмінними. Окрім ціх, рекомендований підхід є використання кейс класів Scala, що є незмінними (якщо ви явно не викажете стан), та чудово робить зі співпадінням шаблонів на боці отримувача.
Ось приклад:
- // визначення кейс класа
- case class Register(user: User)
-
- // створити нове повідомлення з кейс класа
- val message = Register(user)
Надсилання повідомлень
Повідомлення надсилаються акторові через один з наступних методів.
! означає “надіслати-і-забути”, тобто, надіслати повідомлення асинхронно, та безпосередньо повернутись. Також відоме як tell.? надсилає повідомлення асинхронно, та повертає Future, що представляє можливу відповідь. Також відоме як ask.
Порядок повідомлень гарантований на основі кожного надсилача.
Зауваження
Є вплив на продуктивність з використанням ask, оскільки порібно дещо відстежувати на таймаут, дещо потрібне для пов'язання Promise з ActorRef, та це також повинне бути доступним через ремоутинг. Так що завжди схиляйтесь до tell в цілях продуктивності, та ask тільки якщо ви змушені.
Tell: надіслав-забув
Це переважний спосіб надсилати повідомлення. Немає блокуючого очікування повідомлення. Це надає кращу конкурентність та характеристики маштабованості.
Якщо викликається з Actor, тоді посилання на актора, що надсилає, буде неявно передане разом з повідомленням, та доступне отримуючому акторовів його члені-методі sender(): ActorRef. Цільовий актор може використовувати це для відповіді оригінальному надсилачеві, використовуючи sender() ! replyMsg.
Якщо виклик походить з екземпляра, що не є Actor, надсилачем по замовчанню буде посилання на актораdeadLetters.
Ask: надіслати-та-повернути-Future
Шаблон ask включає акторів та майбутнє, таким чином він надається як шаблон використання, скоріше ніж метод на ActorRef:
- import akka.pattern.{ ask, pipe }
- import system.dispatcher // ExecutionContext, що буде використаний
- final case class Result(x: Int, s: String, d: Double)
- case object Request
-
- implicit val timeout = Timeout(5 seconds) // потрібне для `?` нижче
-
- val f: Future[Result] =
- for {
- x <- ask(actorA, Request).mapTo[Int] // прямий виклик предка
- s <- (actorB ask Request).mapTo[String] // виклик через неявне перетворення
- d <- (actorC ? Request).mapTo[Double] // виклик по символічному імені
- } yield Result(x, s, d)
-
- f pipeTo actorD // .. або ..
- pipe(f) to actorD
Цей приклад демонструє ask разом з шаблоном на майбутньому pipeTo, тому що це буде, напевно, буде звичайною комбінацією. Будь ласка, зауважте, що все наведене вище повністю неблокуюче та асинхронне: ask продукує Future, троє з яких компонуються в нове мабйтнє з використанням for-осяжності, та потім pipeTo встановлює onComplete-обробник на майбутньому, щоб вплинути через надходження агрегованого Result на іншого актора.
Використання ask надісле пвідомлення до актора-отримувача, як і при tell, та актор-отримувач має відповісти за допомогою sender()! reply, щоб завершити повернуте Future зі значенням. Операція ask включає створення внутрішнього актора для обробки цієї відповіді, що потребує наявність таймаута, після чого він руйнується, щоб не прогавити ресурси; дивіться нижче щодо додаткової інформації.
Попередження
Щоб завершити майбутнє з виключенням, вам треба надіслати повідомлення Failure до надсилача. Це не робиться автоматично, коли актор підіймає виключення при обробці повідомлення.
- try {
- val result = operation()
- sender() ! result
- } catch {
- case e: Exception =>
- sender() ! akka.actor.Status.Failure(e)
- throw e
- }
Якщо актор не завершує майбутнє, він буде простроченний після періонду таймауту, завершуючи його з AskTimeoutException. Таймаут береться з одного з наступних розміщень в порядку приорітету:
- явно заданий таймаут, як в наступному коді:
- import scala.concurrent.duration._
- import akka.pattern.ask
- val future = myActor.ask("hello")(5 seconds)
- неявний аргумент типу
akka.util.Timeout
- import scala.concurrent.duration._
- import akka.util.Timeout
- import akka.pattern.ask
- implicit val timeout = Timeout(5 seconds)
- val future = myActor ? "hello"
Дивіться Futures для додаткової інформації, щодо того, як очікувати запиту від майбутнього.
Методи onComplete, onSuccess, або onFailure наFuture можуть бути використані для реєстрації зворотнього виклику при завершенні Future, надаючи вам спосіб для уникання блокування.
Повідомлення
При використанні зворотніх викликів майбутнього, таких як onComplete, onSuccess, та onFailure, в акторах вам потрібно уважно уникати замикань на посиланні точуючого актора, тобто, не викликайте методи, або не отримуйте доступ до змінного стану на отосуючому акторі з середини актора. Це може зруйнувати інкапсуляцію, та може призвести до багів синхронізації та стану гонок, оскільки звороній виклик буде запланований конекурентно з охоплюючим актором. На жаль, досі немає способу визначити ці нелегальні доступи під час компіляції. Таокж дивіться: Актори та розподілений змінний стан
Пересилання повідомлень
Ви можете пересилати повідомлення від одного актора іншому. Це означає, що оригінальна адреса/посилання надсилача буде збережена, навіть якщо повідомлення перейде через 'посередника'. Це може бути корисним при написанні акторів, що роблять як маршрутизатори, балансувальники навантаження, реплікатори, тощо.
Отримання повідомлень
Актор має реалізувати метод receive для отримання повідомлень:
- type Receive = PartialFunction[Any, Unit]
-
- def receive: Actor.Receive
Цей метод повертає PartialFunction, тобто вираз ‘match/case’, в якому повідомлення може бути порівняне з різними випадками з використанням співпадінь Scala. Ось приклад цього:
- import akka.actor.Actor
- import akka.actor.Props
- import akka.event.Logging
-
- class MyActor extends Actor {
- val log = Logging(context.system, this)
-
- def receive = {
- case "test" => log.info("received test")
- case _ => log.info("received unknown message")
- }
- }
Відповідь на повідомлення
Якщо ви бажаєте обробити та відповісти на повідомлення, ви можете використати sender(), що поверне ActorRef. Ви можете відповісти надсилаючи ActorRef за допомогою sender() ! replyMsg. Ви також можете зберігти ActorRef для пізнішої відповіді, або передати посилання іншому актору. Якщо немає надсилача (повідомлення було надіслане без актора або майбутнього контексту), тоді надсилач по замовчанню є посилання на актора 'dead-letter'.
- case request =>
- val result = process(request)
- sender() ! result // буде мати актора dead-letter по замовчанню
Таймаут отримання
ActorContext setReceiveTimeout визначає таймаут бездіяльності, після якого спрацює надсилання ReceiveTimeoutmessage. Коли вказане, функцію отримання повинна бути в змозі обробити akka.actor.ReceiveTimeoutmessage. Одна мілісекунда є мінімально підтримуваним таймаутом.
Будь ласка зауважте, що таймаут отриманняможе спрацювати та поставити в чергу повідомлення ReceiveTimeout прямо після іншого повідомлення в черзі; таким чином, не є гарантованим, що під час прийняття таймауту отримання буде попередньо період простою, як сконфігуровано через цей метод.
Коли встановлений, таймаут отримання має дію (тобто, продовжує постійно спрацьовувати після періоду бездіяльності). Передайте inDuration.Undefined для відключення цієї можливості.
- import akka.actor.ReceiveTimeout
- import scala.concurrent.duration._
- class MyActor extends Actor {
- // Щоб задати початкову затримку
- context.setReceiveTimeout(30 milliseconds)
- def receive = {
- case "Hello" =>
- // Щоб встановити відповідь на повідомлення
- context.setReceiveTimeout(100 milliseconds)
- case ReceiveTimeout =>
- // Щоб відключити
- context.setReceiveTimeout(Duration.Undefined)
- throw new RuntimeException("Receive timed out")
- }
- }
Повідомлення, позначені як NotInfluenceReceiveTimeout, не будуть скидати таймер. Це може бути корисним, коли має бути викликаний ReceiveTimeout з зовнішньої неактивності, але не спричиненої внутрішньою активністю, тобто, від спланованих тікових повідомлень.
Зупинка акторів
Актори зупиняються через виклик метода stop на ActorRefFactory, тобто, через ActorContext або ActorSystem. Типово контекст використовується для зупинки самого актора або його дітей, та системи, для зупинення акторів вищого рівня. Справжнє завершення актора виконується асинхронно, stop може повернутись перед завершенням актора.
- class MyActor extends Actor {
-
- val child: ActorRef = ???
-
- def receive = {
- case "interrupt-child" =>
- context stop child
-
- case "done" =>
- context stop self
- }
-
- }
Обробка поточного повідомлення, якщо таке є, буде продовжене, до того, як актор зупиниться, але додаткові повідомлення в поштовій скринці не будуть оброблені. По замовчанню ці повідомлення надсилаються до deadLetters в ActorSystem, але це залежить від реалізації поштової скриньки.
Завершення актора виконується в два кроки: спершу актор призупиняє обробку власної скриньки, та надсилає команду запунки всім дітям, потім він продовжує обробку внутрішніх повідомлень завершення від дітей, до останнього, та потім завершує себе (викликаючи postStop, скидаючи скриньку в дамп, публікуючиTerminated на DeathWatch, та сповіщаючи супервізор). Ця процедура переконує, що піддерева системи акторів завершуються у впорядкований спосіб, просуваючи команду завершення до вузлів, та збираючи їх підтвердження до зупиненого супервізора. Якщо один з акторів не відповідає (обробляє повідомлення довгий період часу, та не відповідає на команду зупинки), весь цей процес може застопоритись.
Під час ActorSystem.terminate захисник системи акторів буде зупинений, та зазначений процес буде гарантувати відповідне завершення цілої системи.
Перехоплювач postStop викликається після того, як актор повністю зупинений. Це дозволяє очищення ресурсів:
- override def postStop() {
- // очищуємо деякі ресурси ...
- }
Зауваження
Оскільки зупинка актора є асинхронною, ви не можете безпосередньо використовувати ім'я дитини, що ви щойно зупинили; це призведе до InvalidActorNameException. Замість цього, watch за актором, та створіть його заміну у відповідь на повідомлення Terminated, що згодом надійде.
PoisonPill
Ви також можете надіслати акторові повідомлення akka.actor.PoisonPill, що зупинить актора при обробці цього повідомлення. PoisonPill стає в чергу як звичайне повідомленя, та буде оброблене після повідомлень, що вже наявні в черзі поштової скриньки.
М'яка зупинка
gracefulStop є корисним, коли вам треба зачекати завершення, або узгодити послідовне завершення декількох акторів:
- import akka.pattern.gracefulStop
- import scala.concurrent.Await
-
- try {
- val stopped: Future[Boolean] = gracefulStop(actorRef, 5 seconds, Manager.Shutdown)
- Await.result(stopped, 6 seconds)
- // актор буде зупинений
- } catch {
- // актор не може зупинитись за 5 секунд
- case e: akka.pattern.AskTimeoutException =>
- }
- object Manager {
- case object Shutdown
- }
-
- class Manager extends Actor {
- import Manager._
- val worker = context.watch(context.actorOf(Props[Cruncher], "worker"))
-
- def receive = {
- case "job" => worker ! "crunch"
- case Shutdown =>
- worker ! PoisonPill
- context become shuttingDown
- }
-
- def shuttingDown: Receive = {
- case "job" => sender() ! "service unavailable, shutting down"
- case Terminated(`worker`) =>
- context stop self
- }
- }
Коли gracefulStop() повертається успішно, буде виконаний перехоплювач актора postStop(): існує межа відбуволось-перед між кінцем postStop(), та поверненням gracefulStop().
В прикладі вище власне повідомлення Manager.Shutdown надіслане до цільового актора, щоб ініціювати процес зупинки актора. Ви можете задіяти для цього PoisonPill, але тоді ви маєте обмежені можливості, щоб виконати взаємодію з іншими акторами перед зупинкою цільового актора. Прості завдання очистки можуть бути оброблені в postStop.
Попередження
Майте на увазі, що коли актор зупиняється, та його ім'я відреєструється, асинхронно відбуваються окремі події від одного до іншого. Таким чином, можливо ви винайдете деяке ім'я все ще задіяним після повернення gracefulStop(). Щоб гарантувати відповідну дереєстрацію, задійте тільки імена від супервізора, що ви контролюєте, та тільки у відповідьна повідомлення Terminated, тобто, не для акторів вищого рівня.
Become/Unbecome
Upgrade
Akka підтримує горячу заміну цилу повідомлень актора (його реалізації) під час виконання: викличте метод context.become з актора. become приймає PartialFunction[Any, Unit], що реалізує новий обробник повідомлень. Замінений по гарячому код утримується в стеку Stack, що може бути заштовханий та виштовханий.
Попередження
Будь ласка, зауважте, що актор не буде повертатись до оригінальної поведінки, коли він рестартований супервізором.
Щоб підмінити поведінку актора по гарячому з використанням become:
- class HotSwapActor extends Actor {
- import context._
- def angry: Receive = {
- case "foo" => sender() ! "I am already angry?"
- case "bar" => become(happy)
- }
-
- def happy: Receive = {
- case "bar" => sender() ! "I am already happy :-)"
- case "foo" => become(angry)
- }
-
- def receive = {
- case "foo" => become(angry)
- case "bar" => become(happy)
- }
- }
Цей варіант метода become корисний для багатьої різних речей, як реалізація Машини Кінечних Станів (Finite State Machine, FSM, наприклад, Dining Hakkers). Це замінить поточну поведінку (верхівку стеку поведінки), що означає, що ви не використовуєте unbecome, але замість цього кожного разу встановлюється нова поведінка.
Інший спосіб викорстанняbecome не заміщує, але додає нагору до стеку поведінок. В цьому випадку теба бути уважним, щоб переконатись, що кількісь операцій “виштовхувань” (unbecome) співпадає з числом “заштовхувань”, в довгій перспективі. Інакше це спричинить витік пам'яті (ось чому ця поведінка не стоїть по замовчанню).
- case object Swap
- class Swapper extends Actor {
- import context._
- val log = Logging(system, this)
-
- def receive = {
- case Swap =>
- log.info("Hi")
- become({
- case Swap =>
- log.info("Ho")
- unbecome() // скидає найбільший 'become' (просто для втіхи)
- }, discardOld = false) // заштовхує нагору, замість заміни
- }
- }
-
- object SwapperApp extends App {
- val system = ActorSystem("SwapperSystem")
- val swap = system.actorOf(Props[Swapper], name = "swapper")
- swap ! Swap // пише Hi
- swap ! Swap // пише Ho
- swap ! Swap // пише Hi
- swap ! Swap // пише Ho
- swap ! Swap // пише Hi
- swap ! Swap // пише Ho
- }
Кодування вкладених отримувань акторів Scala без витоків пам'яті
Дивіться цей приклад отримання невкладених.
Stash
Трейт сховка Stash дозволяє актору тимчасово зберігати повідомлення, що не можуть, або не повинні бути оброблені з використання поточної поведінки актора. Перед зміню обробника повідомлень актора, тобто, прячмо перед виикликом context.become orcontext.unbecome, всі приховані повідомлення можуть бути "відзбережені", так що вони опиняться на початку поштової скриньки актора. Таким чином, приховані повідомлення можуть бути оброблені в тому ж порядку, що вони були отримані.
Зауваження
Трейт Stash розширює маркер-трейт RequiresMessageQueue[DequeBasedMessageQueueSemantics], що опитує систему, щоб автоматично обирати видбір з черги на основі реалізації для актора. Якщо ви бажаєте більше керування над поштовою скринькою, дивіться документацію щодо скриньок: Поштові скриньки.
Ось приклад Stash в дії:
- import akka.actor.Stash
- class ActorWithProtocol extends Actor with Stash {
- def receive = {
- case "open" =>
- unstashAll()
- context.become({
- case "write" => // записуємо...
- case "close" =>
- unstashAll()
- context.unbecome()
- case msg => stash()
- }, discardOld = false) // стек нагору замість заміщення
- case msg => stash()
- }
- }
Виклик stash() додає поточне повідомлення (повідомлення, що актор отримав останнім) до сховку актора. Це типово викликається, коли обробляється випадо по замовчанню, щоб зберігти повідомлення, що не оброблене в інших випадках. Є нелегальним приховувати те ж повідомлення двічі; це призведе до виклику IllegalStateException. Сховок також може бути одмежений, та в цьому випадку виклик stash() може призвести до порушення місткості, та згодом до StashOverflowException. Місткість сховку може бути сконфігурована через stash-capacitysetting (Int) в конфігурації поштової скриньки.
Виклик unstashAll() відкликає повідомлення зі сховку до скриньки актора, доки не буде досягнута, якщо є, місткість поштової скриньки (зауважте, що повідомлення зі сховка стають наперед скриньки). В випадку переповнення обмеженої скриньки, підійметьсяMessageQueueAppendFailedException. Сховок буде гарантовано порожній після викликуunstashAll().
Сховок підтримується через scala.collection.immutable.Vector. Як слідоцтво, навіть кожне велике число повідомлень може бути приховане без значного впливу на продуктивність.
Попередження
Зауважте, що трейт Stash має бути зміксованй до (або до субкласу) терйту Actor, перед тим, як любий трейт або клас перекриє зворотній виклик preRestart. Це означає, що не можливо написати Actor with MyActor with Stash, якщо MyActor перекриває preRestart.
Зауважте, що сховок є частиною ефемерного стану актора, на відміну від поштової скриньки. Таким чином, він має керуватись як інші частини стану актора, що мають ті ж властивості. Реалізація трейту Stash в preRestart буде викликати unstashAll(), що звичайно є бажаною поведінкою.
Зауваження
Якщо ви бажаєте примусити, щоб ваш актор міг робити тільки з необмеженим сховком, вам треба замість цього скористатитсь трейтом UnboundedStash.
Вбивство актора
Ви можете вбити актора, надсилаючи повідомлення Kill. Це спричинить виклик акторомActorKilledException, що дасть збій. Актор призупинить обробку, та його супервізор отримає запит, як обробити цю відмову, що може означати відновлення актора, його рестарт, або його повне завершення. Дивіться Що означає нагляд супервізора для додаткової інформації.
Використовуйте Kill таким чином:
- // вбити актора 'victim'
- victim ! Kill
Актори та виключення
Може трапитись, що під час обробки повідомлення актором, виникне деякий різновид виключення, як виключення бази даних.
Що відбудеться з повідомленням
Якщо відбудеться виключення під час обробки повідомлення (тобто воно вибране зі скриньки, та оброблене поточною поведінкою), тоді повідомлення буде втрачене. Важливо розуміти, що воно не повернеться до поштової скриньки. Так що якщо ви бажаєте повторити обробку повідомлення, вам треба мати справу з цім самотужки, перехоплюючи виключення, та повторюючи все з початку. Будьте впевнені, що ви наклали обмеження на число повторів, оскільки ви не бажаєте загнати систему в постійний цикл (що споживає цикли процесора без жодного прогресу). Інша можливість може полягати в розгляданні шаблону PeekMailbox.
Що відбувається з поштовою скринькою
Якщо під час обробки повідомлення відбувається виключення, з поштовою скринькою нічого не відбувається. Якщо актор рестартує, він отримає ту ж скриньку. Так що всі повідомлення також будуть на місці.
Щоб відбувається з актором
Якщо код актора викликає виключення, цей актор призупиняється, та стартує процес супервізора(дивіться Супервізор та моніторинг). В залежності від рішення супервізора, актор відновлюється (якби нічого не трапилось), рестартує (очищуючи його внутрішній стан, та починаючи з нуля), або завершується.
Розширення акторів за допомогою зчеплення PartialFunction
Іноді може бути корисним розділяти загальну поведінку між декількома акторами, або складати поведінку одного актора з декількох меньших функцій. Це можливо, оскільки метод актора receive повертає Actor.Receive, що є псевдонімом типу для PartialFunction[Any,Unit], та часткові функції можуть бути зчеплені разом, з використанням метода PartialFunction#orElse. Ви можете зціпити стільки функцій, скільки треба, але ви не повинні забувати, що перемагає "перше співпадіння" - що може бути важливо, коли комбінуєте функції, що обоє можуть обробляти той самий тип повідомлень.
Наприклад, уявімо, що ми маємо набір акторів, що є або Producers, або Consumers, при чому іноді має сенс мати актора, що розділяє обоє поведінки. Це можна просто досягти без дублкації кода, виділяючи поведінки у трейти, та реалізуючи receive акторів як комбінацію ціх часткових функцій.
- trait ProducerBehavior {
- this: Actor =>
-
- val producerBehavior: Receive = {
- case GiveMeThings =>
- sender() ! Give("thing")
- }
- }
-
- trait ConsumerBehavior {
- this: Actor with ActorLogging =>
-
- val consumerBehavior: Receive = {
- case ref: ActorRef =>
- ref ! GiveMeThings
-
- case Give(thing) =>
- log.info("Got a thing! It's {}", thing)
- }
- }
-
- class Producer extends Actor with ProducerBehavior {
- def receive = producerBehavior
- }
-
- class Consumer extends Actor with ActorLogging with ConsumerBehavior {
- def receive = consumerBehavior
- }
-
- class ProducerConsumer extends Actor with ActorLogging
- with ProducerBehavior with ConsumerBehavior {
-
- def receive = producerBehavior.orElse[Any, Unit](consumerBehavior)
- }
-
- // протокол
- case object GiveMeThings
- final case class Give(thing: Any)
Замість наслідування, той же шаблон може бути застосований через композицію - дехто може просто скомпонувати метод receive, з використанням часткові функції від делегатів.
Шаблони ініціалізації
Багаті перехоплювачі життєвого циклу акторів провадять корисний набір інструментів для реалізації різних шаблонів ініціалізації. На протязі життєвого циклу ActorRef, актор може потенційно пройти через декілька рестартів, коли старий примірник замінюється на новий, непомітно для зовнішнього спостерігача, що бачить тільки ActorRef.
Дехто може думати про новий примірник, як про "інкарнації". Ініціалізація може бути необхідною для кожної інкарнації актора, але іноді дехто потребуватиме, щоб ініціалізація відбувалась тільки ри народженні першого примірника, коли створюється ActorRef. Наступні розділи провадять шаблони для різних потреб ініціалізації.
Ініціалізація через конструктор
Використання конструктора для ініціалізації має різні вигоди. Зпершу, це робить можливим використання полів val для зберігання любого стану, що не змінюється на протязі життя примірника актора більш міцним. Конструктор викликається за кожної інкарнації актора, таким чином вміст актора може завжди припускати, що відбулась відповідна ініціалізація. Це також є недоліком цього підходу, оскільки є випадки, коли дехто бажає уникнути повторної ініціалізації вмісту під час рестарту. Наприклад, це часто є корисним для зберігання дитячих акторів між рестартами. Наступний розділ провадить шаблон для цього випадку.
Ініціалізація через preStart
Метод актора preStart() напряму викликається тільки один раз, під час ініціалізації першого примірника, тобто, при створенні його ActorRef. В випадку рестартів, preStart() викликається з postRestart(), і таким чином, якщо не перекритий, preStart() викликається для кожної інкарнації. Однак перекриваючи postRestart() ви можете відключити цю поведінку, та забезпечити, що відбувається тільки один виклик до preStart().
Одне корисне використання цього шаблону є відключення створення нових ActorRefs для дітей під час рестартів. Це може бути досягнуте перекриттям preRestart():
- override def preStart(): Unit = {
- // Ініціалізуйте дітей тут
- }
-
- // Перекриття postRestart щоб відключити виклик до preStart()
- // після рестартів
- override def postRestart(reason: Throwable): Unit = ()
-
- // Реалізація по замовчанню preRestart() зупиняє всіх дітей
- // актора. Щоб відмовитись від зупинки дітей, ми маємо
- // перекрити preRestart()
- override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
- // Зберігайте виклик postStop(), але не зупуняємо дітей
- postStop()
- }
Будь ласка зауважте, що дитячі актори все ще рестартують, але без створення нового ActorRef. Дехто може застосувати рекурсивно ті ж принципи для дітей, гарантуючи, що їх метод preStart() викликається тільки при створенні їх посилань.
For more information see What Restarting Means.
Ініціалізація через надсилання повідомлення
Є випадки, коли неможливо передати потрібну для ініціалізації інформацію в конструктор, наприклад, в присутності кругових залежностей. В цьому випадку актор повинен очікувати повідомлення ініціалізації, та використовуйте become(), або перехід від кінечних до машинних станів, щоб закодувати ініціалізовані та неініціалізовані стані актора.
- var initializeMe: Option[String] = None
-
- override def receive = {
- case "init" =>
- initializeMe = Some("Up and running")
- context.become(initialized, discardOld = true)
-
- }
-
- def initialized: Receive = {
- case "U OK?" => initializeMe foreach { sender() ! _ }
- }
Якщо актор може отримувати повідомлення перед тим, як він буде ініціалізований, корисни мінструментом може виявитись Stash для збереження повідомлень, доки не завершиться ініціалізація, та програвання їх після того, як автор буде ініціалізований.
Попередження
Цей шаблон має використовуватись з обережністю, та застосовується тільки коли жодний з шаблонів вище не може бути застосований. Однією з потенційних проблем може бути втрата повідомлень при надсиланні до віддалених акторів. Також публікація ActorRef в неініціалізованому стані актора може призвести до умов, коли він отримує користувацькі повідомлення перед тим, як буде виконана ініціалізація.
Akka Typed
Попередження
Цей модуль наразі є експериментальним, в тому сенсі, що є предметом активної розробки. Це означає, що API або семантика може змінитись без попередження або періода амортизації, тому не рекомендано використовувати цей модуль в виробництві прямо зараз — вас попередили.
Як дискутовано в Системах акторів (та наступних главах) актори існують заради надсилання повідомлень між незалежними одиницями обчислень, але як це виглядає? Все подальше очікує, що імпортовані наступні бібліотеки:
- import akka.typed._
- import akka.typed.ScalaDSL._
- import akka.typed.AskPattern._
- import scala.concurrent.Future
- import scala.concurrent.duration._
- import scala.concurrent.Await
Коли це на місці, ми можемо визначити нашого першого актора, та, звичайно, ми скажемо привіт!
- object HelloWorld {
- final case class Greet(whom: String, replyTo: ActorRef[Greeted])
- final case class Greeted(whom: String)
-
- val greeter = Static[Greet] { msg =>
- println(s"Hello ${msg.whom}!")
- msg.replyTo ! Greeted(msg.whom)
- }
- }
Цей малий шматок кода визначає два типи повідомлень, один що наказує актору привітати декого, та другий актор буде використовуватись для підтвердження, що це зроблене. Тип Greet містить не тільки інформацію про те, кого привітати, він також містить ActorRef, що постачає надсилач повідомлення, так що актор HelloWorld може надіслати назад повідомлення з підтвердженням.
Поведінка актора визначена як значення greeter за допомогою конструктора поведінки Static — є багато різних шляхів зформулювати поведінки, як ми побачимо в подальшому. “Статична” поведінка не здатна вмінюватись у відповідь на повідомлення, вона залишатиметься такою ж, доки актор не буде зупинений батьком.
Тип повідомлення, що обробляється цією поведінкою, декларований як клас Greet, що має на увазі, що аргумент наданої функції msg також такого типу. Ось чому ми можемо отримати доступ до членів whom та replyTo, без потреби використовувати порівняння з шаблоном.
В останньому рядку ми бачимо, як актор HelloWorld надсилає повідомлення іншому акторові, що робиться за допомогою оператора ! (промовляється “tell”). Оскільки адреса replyTo декларована як тип ActorRef[Greeted], компілятор буде дозволяти нам надсилать повідомлення тільки цього типу, інше використання не буде сприятись.
Прийнятні типи повідомлень актора, разом зі всіма типами відповідей визначають протокол, яким розмовляє цей актор; в цьому випадку це простий протокол запиту-відповіді, але актори в разі потреби можуть моделювати довільно складні протоколи. Протокол запакований разом з поведінкою, що реалізує його в гарно огорнутому полі зору — об'єкті HelloWorld.
Тепер ми бажаємо випробувати цього актора, так що ми маємо запустити систему акторів, де він розміститься:
- import HelloWorld._
- // використовуємо глобальний пул, бо ми бажаємо виконувати речі післі system.terminate
- import scala.concurrent.ExecutionContext.Implicits.global
-
- val system: ActorSystem[Greet] = ActorSystem("hello", Props(greeter))
-
- val future: Future[Greeted] = system ? (Greet("world", _))
-
- for {
- greeting <- future.recover { case ex => ex.getMessage }
- done <- { println(s"result: $greeting"); system.terminate() }
- } println("system terminated")
Після імпорту визначення протокола актора, ми запускаємо систему акторів з визначеною поведінкою, огортаючи її в Props, як актора на сцені. Властивості props, що ми передаємо до цієї системи, є тільки замовченнями. Ми можемо в цій точці також сконфігурувати, як та де актор повинен бути розгорнений в кластерній системі.
Як каже Карл Хьювіт, один актор ще не актор — він буде досить самотнім, коли ні з ким поговорити. В сенсі нашого приклада, це трохи жорстко, оскільки ми надали актору тільки HelloWorld підробну особу для ромов - шаблон “ask” (представлений оператором ? ), що може бути використаний для надсилання повідомлень, так, щоб відповідь відповідала Promise, до якого ми повертаємо відповідне Future.
Зауважте, що це Future, що повертається операцією “ask”, вже відповідно типізоване, перевірка типу або приведення не потрібні. Це можливо через інформацію типу, що є частиною протокола повідомлень: оператор ? приймає як аргумент функцію, що приймає ActorRef[U] (що пояснює дірку _ в виразі на рядку 6 вище), та параметр replyTo, що ми заповнюємо як такий, що має тип ActorRef[Greeted], що означає, що значення, що задовільняє Promise, може бути тільки типу Greeted.
Ми використовуємо це тут для надсилання команди Greet актору, та потім для відповідь надходить назад, ми її роздруковуємо, та наказуємо системі акторів зупинитись. Коли це також зроблене, ми друкуємо повідомлення "system terminated", та програма завершується. Комбінатор recovery на оригінальному Future потрібен, щоб переконатись в відповідньому завершенні системи, навіть якщо щось піде не так; комбінатори flatMap та map для вираза for починають турбуватись тільки щодо “щасливого шляху”, та якщо future схибить через таймаут, тоді greeting не буде виділений, та нічого взагалі не трапиться.
Це показує, що є аспекти повідомлень акторів, що можуть бути перевірені на тип компілятором, але ця можливість не безмежна, є обмеження того, що ми можемо виразити статично. Перед тим, як ми перейдемо до більш складного (та реалістичного) приклада, ми зробимо малий відступ, щоб висвітлити деяку теорію позаду всього цього.
Крихта теорії
Модель акторів, як визначено Хьювітом, Бішопом та Стайгером в 1973му році, є обчислювальною моделлю, що виражає саме те, що це означає бути розподіленим для обчислень. Одиниці обробки, актори, можуть комунікувати тільки через обмін повідомленнями, та при прийомі повідомлення актор може виконати три наступні фундаментальні дії:
- надіслати скінчене число повідомлень акторам, яких він знає
- створити скінчене число нових акторів
- визначити поведінку, що буде застосована до наступного повідомлення
Проект Akka Typed виражає ці дії з використанням поведінок та адрес. Повідомлення можуть бути надіслані за адресами, та за цім фасадом існує поведінка, що отримує повідомлення, та діє на його основі. Зв'язування між адресами та поведінкою може змінюватись з часом, відповідно до третього пункту вище, але це не видиме ззовні.
З такою преамбулою ми можемо перейти до унікальної властивості цього проекту, точніше, саме те, що від вводить статичну перевірку взаємодії акторів: адреси параметризовані, та ім можуть надсилатись тільки повідомлення, що мають певний тип. Асоціація між адресами та їх параметром типа, мусить бути зроблена, коли створюються адреса (та її актор). З цією ціллю кожна поведінка також параметризована типом повідомлень, що вона може обробляти. Оскільки поведінка може змінюватись за фасадом адреси, визначення наступної поведінки є обмеженою операцією: послідовник мусить обробляти той же тип повідомлень, що і попередник. Це необхідно, щоб не зруйнувати адреси, що посилаються на актора.
Що це дає, це коли повідомлення надсилається до актора, де ми можемо статично переконатись, що тип повідомлення одне з тих, що актор декларує як оброблювані — ми можемо уникнути помилки надсилання повністю безглуздих повідомлень. Однак в чому ми не можемо перконатись, це в тому, що поведінка поза адерсою буде в певносу стані, коли повідомлення буде отриманим. Фундаментальна причина є в тому, що асоціація між адресою та поведінкою є динамічною властивістю часу виконання, та компілятор не може знати цього, доки він не транслює початковий код.
Це те ж саме, що і для нормальних об'єктів Java з внутрішніми змінними: при компіляції програми ми не можемо знати, якими будуть їх значення, та чи результат виклику метода залежить від тих змінних, бо результат непевний до певної міри —ми можемо тільки бути певними, що повернуте значення є даного типа.
Ми бачили вище, що тип результату команди актора описаний типом адереси reply-to, тобто, міститься в повідомленні. Це дозволяє, щоб перетворення було описане в термінах типів: відповідь буде типа A, але вона може також містити адресу типу B, що потім дозволяє іншому актору продовжити розмову, надсилаючи повідомлення типу B цьому новому акторові. Хоча ми не можемо статично виразити “поточний” стан актора, ми можемо виразити поточний стан протокола між двома акторами, оскільки він надається типом останнього повідомлення, що було отримане або надіслане.
В наступному розділі ми продемонструємо це на більш реалістичному прикладі.
Більш складний приклад
Розглянемо актора, що керує кімнатою чату: клієнтські актори можуть з'єднатись, надсилаючи повідомлення, що містить їх екранне ім'я, та після цього вони можуть надсилати повідомлення. Актор чат кімнати буде розбирати всі надіслані повідомлення до всіх наразі під'єднаних клієнтськіх акторів. Визначення протокола може виглядати таким чином:
- sealed trait Command
- final case class GetSession(screenName: String, replyTo: ActorRef[SessionEvent])
- extends Command
-
- sealed trait SessionEvent
- final case class SessionGranted(handle: ActorRef[PostMessage]) extends SessionEvent
- final case class SessionDenied(reason: String) extends SessionEvent
- final case class MessagePosted(screenName: String, message: String) extends SessionEvent
-
- final case class PostMessage(message: String)
Зпочатку клієнтській актор отримуює доступ тільки до ActorRef[GetSession], що дозволяє їм зробити перший крок. Коли клієнтська сессія встановлена, вit отримує повідомлення SessionGranted, що містить handle, щоб розблокувати наступний крок протокола, публікацію повідомлень. Команда PostMessage буде надсилатись на цю окрему адресу, що представляє сесію, якак додається до кімнати чата. Інший аспект сесії в тому, що клієнт розкриває свою власну адресу через аргумент replyTo, так що наступні події MessagePosted можуть надсилатись до нього.
Це ілюструє, як актори можуть виражати більше, ніж тільки еквівалент виклику метода для Java об'єктів. Декларовані типи повідомлення та їх вміст описують повний протокол, що може залучати декіклька акторів, та що може включати декіклька кроків. Реалізація протокола кімнати чата може бути такою простою, як це:
- private final case class PostSessionMessage(screenName: String, message: String)
- extends Command
-
- val behavior: Behavior[GetSession] =
- ContextAware[Command] { ctx =>
- var sessions = List.empty[ActorRef[SessionEvent]]
-
- Static {
- case GetSession(screenName, client) =>
- sessions ::= client
- val wrapper = ctx.spawnAdapter {
- p: PostMessage => PostSessionMessage(screenName, p.message)
- }
- client ! SessionGranted(wrapper)
- case PostSessionMessage(screenName, message) =>
- val mp = MessagePosted(screenName, message)
- sessions foreach (_ ! mp)
- }
- }.narrow // тільки показує GetSession назовні
Ядро цієї поведінки знов статичне, сама кімната чату не змінюється в щось інше, коли встановлюються сесії, але ми вводимо змінну, що відслідковує відкриті сесії. Коли надходить нова команда GetSession, ми додаємо цього клієнта до списку, та потім нам треба створити ActorRef сесії, що буде використане для публікації повідомлень. В цьому випадку ми бажаємо створити дуже простого актора, що просто перепаковує команду PostMessage на команду PostSessionMessage, що також включає екранне ім'я. Такий актор-огортка може бути створена з використанням метода spawnAdapter на ActorContext, так що потім ми можемо перейти до відповіді клієнту з результатом SessionGranted.
Поведінка, що ми тут декларуємо, може обробляти обоє підтипи Command. GetSession вже був пояснений, та команди PostSessionMessage, що походять від акторів-огорток, будуть перемикати поширення повідомлень чат кімнати на всіх під'єднаних клієнтів. Але ми не бажаємо надати можливість надсилати командиPostSessionMessage до довільних клієнтів, ми резервуємо це право для оболонок, що ми створюємо — інакше клієнти зможуть видавати себе за повністі інші екранні імена (уявіть протокол GetSession, що включає інформацію аутентифікації, що ще унебезпечує це). Таким чином, ми обмежуємо поведінку до тільки прийняття команд GetSession, перед тим, як представити їх світу, і, таким чином, тип значення behavior є Behavior[GetSession], замість Behavior[Command].
Звуження типу поведінки завжди безпечна операція, оскільки вона тільки обмежує, що може робити клієнт. Якщо ми розширимо тип, тоді клієнти можуть надсилати інші повідомлення, що не очікувались при написанні первинного кода для поведінки.
Якщо ми не стурбовані безпекою відповідності між сесією та екранним ім'ям, ми можемо змінити протокол, видаливши PostMessage, та всі клієнти отримають тількиActorRef[PostSessionMessage] куди треба надсилати. В цьому випадку не потрібна огортка, та ми можемо просто використовувати ctx.self. Перевірки типу відробляють в цьому віпадку, оскільки ActorRef[-T] є контрваріантним в його параметрі типа, що означає, що ми можемо використосувати ActorRef[Command]кожного разу, коли потрібен ActorRef[PostSessionMessage] — це має сенс, оскільки попередній просто розмовляє більшими мовами, ніж останній. Навпаки буде проблематичним, так що передача ActorRef[PostSessionMessage] де потрібнеActorRef[Command] призведе до помилки типа.
Останній шматок цього визначення поведінки є декоратор ContextAware, що ми використовуємо, щоб отримати доступ до ActorContext в Static визначенні поведінки. Цей декоратор викликає запроважену функцію , коли отримане перше повідомлення, та, таким чином. створює реальну поведінку, що буде використовуватись в подальшому — декоратор відкидається, після того, як зробив свою роботу.
Спробуємо це
Щоб побачити цю чат кімнату в дії нам треба написати клієнта-актора, що буде використовувати її:
- import ChatRoom._
-
- val gabbler: Behavior[SessionEvent] =
- Total {
- case SessionDenied(reason) =>
- println(s"cannot start chat room session: $reason")
- Stopped
- case SessionGranted(handle) =>
- handle ! PostMessage("Hello World!")
- Same
- case MessagePosted(screenName, message) =>
- println(s"message has been posted by '$screenName': $message")
- Stopped
- }
З цієї поведінки ми можемо створити актора, що буде приймати сесію чат кімнати, надсилати повідомлення, очікувати, щоб побачити що воно обубліковане, та потім завершуватись. Останній крок потребує можливості змінити поведінку, нам треба перейти від нормальної робочої поведінки до стану завершення. Ось чому актор використовує конструктор різних поведінок з назвою Total. Цей конструктор отримує як аргумент функцію від обробленого типу повідомлення, в цьому випадку SessionEvent, до наступної поведінки. Ця наступна поведінка мусить знову бути того ж типа, як ми обсудили в теоретичному розділі вище. Тут ми або залишаємось в тій самій поведінці, або ми завершуємось, та обоє ці випадки є такими загальними, що є спеціальні значення Same та Stopped, що ми можемо використовувати. Поведінка названа “total” (на відміну від “partial”), оскільки задекларована функція мусить обробляти всі значення її вхідного типа. Оскільки SessionEvent є зв'язаним трейтом, компілятор Scala буде попереджати, якщо ми забудемо обробити один з його підтипів; в цьому випадку він нагадає нам, що альтернативно до SessionGranted ми можето також отримати подію SessionDenied.
Тепер, щоб спробувати це, ми маємо запустити обоє, чат кімнату та gabbler, та, звичайно, ми робимо це в системі акторів. Оскільки може бути тільки один охоронець-супервізор, ми можемо або стартувати чат з gabbler (що для нас небажано — це ускладнює його логіку), або gabbler з чат кімнати (що безглуздо), або ми стартуємо обоє з третього актора — наш єдиний логічний вибір:
- val main: Behavior[Unit] =
- Full {
- case Sig(ctx, PreStart) =>
- val chatRoom = ctx.spawn(Props(ChatRoom.behavior), "chatroom")
- val gabblerRef = ctx.spawn(Props(gabbler), "gabbler")
- ctx.watch(gabblerRef)
- chatRoom ! GetSession("ol’ Gabbler", gabblerRef)
- Same
- case Sig(_, Terminated(ref)) =>
- Stopped
- }
-
- val system = ActorSystem("ChatRoomDemo", Props(main))
- Await.result(system.whenTerminated, 1.second)
В гарних традиціях, ми викликаємо актора main, чим він і є, напряму відповідаючи методу main в традиційних застосуваннях Java. Цей актор буде виконувати свою роботу за його власним бажанням, нам не треба надсилати повідомлення ззовні, так що ми вирішуємо, щоб він був типа Unit. Актори отримують не тільки зовнішні повідомлення, вони також повідомляються по певні системні події, так звані сигнали. Щоб отримати доступ до них, ми обираємо реалізувати цього окремого актора з використанням декоратора поведінки Full. Ім'я походить від факта, що в ньому ми маємо повний доступ до всіх аспектів актора. Запроваджена функція буде викликана для сигналів (огорнутих в Sig) або користувацьких повідомлень (огорнутих в Msg), та огортка також містить посилання на ActorContext.
Цей окремий актор main реагує на два сигнали: коли він запускаєтьяс, він спочатку отримає сигнал PreStart, під час чого створюються чат кімната та gabbler, та ініціюється сесія між ними, та коли gabbler завершується, ми отримаємо подію Terminated, тому що викликали ctx.watch для нього. Це дозволяє нам завершити систему акторів: коли актор main завершується, більше нічого робити.
Таким чином, після створення системи акторів з Props актора main, нам залишається тільки очікувати на його завершення.
Статус цього проекту та відношення до Akka Actors
Akka Typed є результатом багатьох років досліджень та спроб (включаючи Typed Channels в серії 2.2.x), та він на шляху до стабілізації, але дозрівання такої глибинної зміни до ядра концепції Akka займе довгий час. Ми очікуємо, що цей моудль буде залишатись експериментальним для декільког головних релізів Akka, та звичайний akka.actor.Actor не буде амартизований або піде зі сцени в найближчому часі.
Факт, що це дослідницькій проект, також тягне за собою факт, що документація посилання не така деталізована, якою вона буде для фінальної версії, посилайтесь до документації API щодо більш глибоких та точніших деталей.
Головні відмінності
Найбільш яскрава відмінність полягає в видаленні функціональності sender(). Це виявилося ахилесовою п'ятою проекта Typed Channels, це можливість, що робить її сигнатури типів та такорси дуже складними, щоб вони були життєспроможніми. Рішення, прийняте в Akka Typed є явно включити відповідно типізовану адресу reply-to в повідомлення, що обоє, обтяжує користувача цім завданням, але аткож покладає новий аспект розробки протокола, до якого нележить.
Інша яскрава відмінність є видалення трейта Actor. Щоб уникнути замикання над нестабільними посиланнями від різних контекстів виконання (наприклад, трансформацій Future), ми перетворили всі залишкі методів, що були на цьому трейті, на повідомлення: поведінка отримує ActorContext як аргумент під час обробки, та перехоплення життєвого циклу перетворились на Signal.
Побічний ефект цього в тому, що поведінки тепер можуть бути протестовані в ізоляції, без того, щоб пакуватись в актора, тести можуть робити повністю синхронно, не турбуючись щодо таймаутів та підробних збоїв. Інший побічний ефект в тому, що поведінки можуть гарно бути скомпоновані та декоровані, дивіться комбінатори And, Or, Widened, ContextAware; тут немає нічного особливого або поємного, нові комбінатори можуть бути написані як звонішні бібліотеки, або закроєні під кожний проект.
Стійкість до відмов
Як пояснювалось в Системах акторів, кожний актор є супервізором для дітей, та як такий кожний актор визначає стратегію обробки відмов супервізора. Ця стратегія не може бути змінена після цього, бо є невід'ємною частиною структури системи акторів.
Обробка відмов на практиці
Перше, давайте поглянемо на приклад, що ілюструє один зі шляхів до обробки помилок сховища даних, що є типовим джерелом відмов в застосуваннях реального світу. Звичайно, це залежить від конкретного застосування, що можна зробити, коли сховище даних недосяжне, але в цьому прикладі ми використовуємо кращі намагання в підході пере-з'єднання.
Прочитайте наступний код. Коментарі в тексті пояснюють різні частини обробки збоїв, та чому вони додані. Також дуже рекомендоано виконати цей приклад, тому що просто слідувати виводу журнала, щоб зрозуміти, що відбувається під час виконання.
Створення стратегії супервізора
Наступні розділи більш заглиблено пояснюють механізм обробки відмов, та альтернативи.
В цілях демонстрації давайте розглянемо наступну стратегію:
- import akka.actor.OneForOneStrategy
- import akka.actor.SupervisorStrategy._
- import scala.concurrent.duration._
-
- override val supervisorStrategy =
- OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
- case _: ArithmeticException => Resume
- case _: NullPointerException => Restart
- case _: IllegalArgumentException => Stop
- case _: Exception => Escalate
- }
Я обрав декілька гарно відомих типів виключень, щоб продемострувати застосування директив обробки збоїв, описаних в Супервізор та мониторинг. Для початку, це стратегія кожний-за-себе (one-for-one), цо означає, що кожна дитина трактується окремо (стратегія всі-за-одного (all-for-one) робить дуже подібно, з однією різницею, що кожне рішення стосується до всіх дітей супервізора, те тільки до того, хто схибив). Існуює набір обмежень щодо частоти рестартів, наразі 10 рестартів на хвилину; кожне з ціх налаштувань можна опустити, що означає, що відповідний ліміт не застосовується, що лишає можливість вказати абсолютний верхній ліміт на рестарти, або щоб рестарти робили без обмежень. Якщо досягнуто ліміта, дитячий актор зупиняється.
Твердження match, що формує основу тіла, є типу Decider, що те саме, що PartialFunction[Throwable,Directive]. Цей шматок відображує типи відмов дітей на відповідні директиви.
Зауваження
Якщо стратегія декларована в акторі супервізора (на відміну від об'єкта-компан'она), його вирішувач має доступ до всього внутрішнього стана актора в потоко-безпечний спосіб, включаючи отримання посилання на тільки що схибивше дитя (доступно як sender повідомлення про відмову).
Стратегія супервізора по замовчанню
Escalate використовується, якщо визначена стратегія не охоплює виключення, що будо підійняте.
Коли стратегія супервізора не визначена для актора, наступні виключення обробляються по замовчанню:
ActorInitializationException зупиняє викликавшу дитину-актораActorKilledException зупиняє викликавшу дитину-актораException рестартує викликавшу дитину-актора- Інші типи
Throwable будуть передані до батьківського актора
Якщо виключення ескалується весь час, до самого кореневого охоронця, він буде обробляти його в той же спосіб, що і стратегія по замовчанню, визначена вище.
Ви можете комбінувати вашу власну стратегію зі стратегією по замовчанню:
- import akka.actor.OneForOneStrategy
- import akka.actor.SupervisorStrategy._
- import scala.concurrent.duration._
-
- override val supervisorStrategy =
- OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
- case _: ArithmeticException => Resume
- case t =>
- super.supervisorStrategy.decider.applyOrElse(t, (_: Any) => Escalate)
- }
Зупинка стратегії супервізора
Ближчий до Erlang спосіб є стратегія просто зупиняти дітей, коли вони схибили, та потім приймати дії з корегування в супервізорі, коли DeathWatch сигналізує про втрату дитини. Ця стратегія також провадиться з коробки як SupervisorStrategy.stoppingStrategy у супровіді з конфігуратором StoppingSupervisorStrategy, що буде використовуватись, коли ви бажаєте, щоб захисник "/user" застосував її.
Журналювання збоїв акторів
По замовчанню SupervisorStrategy журналює збої, тільки якщо вони не еккалюють. Ескальовані збої розглядаються як оброблені, та потенційно журнальовані, на вищому рівні ієрархії.
Ви можете змінити журналювання по замовчанню SupervisorStrategy, встановивши loggingEnabled в false при створенні примірника. Власне журналювання може бути зроблене всередині Decider. Зауважте, що посилання на поточне збійне дитя доступне як sender, колиSupervisorStrategy задеклароване всередині актора наглядача.
Ви можете також налаштувати журналювання в вашій власній реалізації SupervisorStrategy, перевизначивши методlogFailure.
Нагляд за акторами вищого рівня
Актори вищого рівня означають ті, що створені з використанням system.actorOf(), та вони діти Користувацького захисника. Немає особливих правил, що стосуються цього випадка, захисник просто застосовує сконфігуровану стратегію.
Тестове застосування
Наступний розділ показує ефекти різних директив на практиці, там, де треба тестове налаштування. Для початку, нам треба підходящий супервізор:
- import akka.actor.Actor
-
- class Supervisor extends Actor {
- import akka.actor.OneForOneStrategy
- import akka.actor.SupervisorStrategy._
- import scala.concurrent.duration._
-
- override val supervisorStrategy =
- OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
- case _: ArithmeticException => Resume
- case _: NullPointerException => Restart
- case _: IllegalArgumentException => Stop
- case _: Exception => Escalate
- }
-
- def receive = {
- case p: Props => sender() ! context.actorOf(p)
- }
- }
Цей супервізор буде використаний для створення дітей, з якими ми можемо експериментувати:
- import akka.actor.Actor
-
- class Child extends Actor {
- var state = 0
- def receive = {
- case ex: Exception => throw ex
- case x: Int => state = x
- case "get" => sender() ! state
- }
- }
Тест простіший з використанням утіліт, описаних в Тестуванні систем акторів.
- import com.typesafe.config.{ Config, ConfigFactory }
- import org.scalatest.{ FlatSpecLike, Matchers, BeforeAndAfterAll }
- import akka.testkit.{ TestActors, TestKit, ImplicitSender, EventFilter }
-
- class FaultHandlingDocSpec(_system: ActorSystem) extends TestKit(_system)
- with ImplicitSender with FlatSpecLike with Matchers with BeforeAndAfterAll {
-
- def this() = this(ActorSystem(
- "FaultHandlingDocSpec",
- ConfigFactory.parseString("""
- akka {
- loggers = ["akka.testkit.TestEventListener"]
- loglevel = "WARNING"
- }
- """)))
-
- override def afterAll {
- TestKit.shutdownActorSystem(system)
- }
-
- "A supervisor" must "apply the chosen strategy for its child" in {
- // тут код
- }
- }
Давайте створимо акторів:
- val supervisor = system.actorOf(Props[Supervisor], "supervisor")
-
- supervisor ! Props[Child]
- val child = expectMsgType[ActorRef] // отирмати відповідь від TestKit testActor
Перший тест повинен демонструвати директиву Resume, так що ми спробуємо це, через задання деякого не-первинного стану в акторі, та змусимо його схибити:
- child ! 42 // встановити стан в 42
- child ! "get"
- expectMsg(42)
-
- child ! new ArithmeticException // зламати
- child ! "get"
- expectMsg(42)
Як ми бачимо, значення 42 витримує директиву обробки відмови. Тепер, якщо ми змінимо відмову до більш серйозної NullPointerException, це вже не буде таким:
- child ! new NullPointerException // зламаємо краще
- child ! "get"
- expectMsg(0)
Та нарешті, в випадку фатального IllegalArgumentException дитя буде завершене супервізором:
- watch(child) // нехай testActor наглядає за “child”
- child ! new IllegalArgumentException // ламаємо
- expectMsgPF() { case Terminated(`child`) => () }
До сих пір супервізор був повністю нечутливий до відмов дітей, оскільки набір директив обробляв їх. в випадку Exception, це вже не так, та супервізор ескалує відмову.
- supervisor ! Props[Child] // створити нове дитя
- val child2 = expectMsgType[ActorRef]
- watch(child2)
- child2 ! "get" // перевірити, чи воно живе
- expectMsg(0)
-
- child2 ! new Exception("CRASH") // ескалувати відмову
- expectMsgPF() {
- case t @ Terminated(`child2`) if t.existenceConfirmed => ()
- }
Сам супервізор наглядається високорівневим актором, що провадиться від ActorSystem, що має політику по замовчанню рестартувати, в випадку всіх Exception (з поважним виключенням ActorInitializationException таActorKilledException). Оскільки директива по замовчанню в випадку рестарта вбивати дитей, ми очікуємо, що наші бідні діти не виживуть після цієї відмови.
У випадку, коли це не бажано (що залежить від випадку використання), нам треба використати інший супервізор, що перевизначає цю поведінку.
- class Supervisor2 extends Actor {
- import akka.actor.OneForOneStrategy
- import akka.actor.SupervisorStrategy._
- import scala.concurrent.duration._
-
- override val supervisorStrategy =
- OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
- case _: ArithmeticException => Resume
- case _: NullPointerException => Restart
- case _: IllegalArgumentException => Stop
- case _: Exception => Escalate
- }
-
- def receive = {
- case p: Props => sender() ! context.actorOf(p)
- }
- // перевизначити замовчання вбивати дітей під час рестарту
- override def preRestart(cause: Throwable, msg: Option[Any]) {}
- }
З таким батьком дитя переживатиме ескальований рестарт, як демонструє цей тест:
- val supervisor2 = system.actorOf(Props[Supervisor2], "supervisor2")
-
- supervisor2 ! Props[Child]
- val child3 = expectMsgType[ActorRef]
-
- child3 ! 23
- child3 ! "get"
- expectMsg(23)
-
- child3 ! new Exception("CRASH")
- child3 ! "get"
- expectMsg(0)
Диспечери
Akka MessageDispatcher - це те, що робить акторів Akka Actors "живими", це двигун машини, так що поговоримо про них. Всі реалізаціїMessageDispatcher є також ExecutionContext, що означає, що можуть бути використані для виконання довільного кода, наприклад, Future.
Диспечер по замовчанню
Кожна ActorSystem буде мати диспечер по замовчанню, що буде використаний, коли нічого іншого не сконфігуровано для Actor. Диспечер по замовчанню може бути сконфігурований, та по замовчанню Dispatcher з вказаним default-executor. Якщо ActorSystem створена з переданим ExecutionContext, цей ExecutionContext буде використаний як екзекутор по замовчанню для всіх диспечерів в цій ActorSystem. Якщо ExecutionContext не надано, він відкотиться назад до екзекутора, вказаного в akka.actor.default-dispatcher.default-executor.fallback. По замовчанню це "fork-join-executor", що дає чудову продуктивність в більшості випадків.
Пошук диспечера
Диспечери реалізують інтерфейс ExecutionContext, та можуть, таким чином, бути використані для викликів Future etc.
- // for use with Futures, Scheduler, etc.
- implicit val executionContext = system.dispatchers.lookup("my-dispatcher")
Встановлення диспечера для актора
В випадку, коли ви бажаєте надати вашому актору іншого диспечера, ніж по замовчанню, нам потрібно зробити дві речі, з яких перша є конфігурація диспечера:
- my-dispatcher {
- # Dispatcher є ім'ям базованого на подіях диспечера
- type = Dispatcher
- # Який тип ExecutionService використовувати
- executor = "fork-join-executor"
- # Конфіфгупація для пула fork-join
- fork-join-executor {
- # Мінімальне число потоків, щоб покрити число паралелізма на основі множника
- parallelism-min = 2
- # Паралелізм (потоки) ... ceil(available processors * factor)
- parallelism-factor = 2.0
- # Максимальне число потоків, щоб покрити число паралелізма на основі множника
- parallelism-max = 10
- }
- # Пропускна спроможність максимальне число повідомлень, що будуть оброблятись
- # на кожного актора, перед тим, як потік перестрибне на нового актора.
- # Встановіть 1 для максимальнї чесності.
- throughput = 100
- }
Зауваження
Зауважте, що parallelism-max не встановлює верхню межу не загальне число потоків, розміщених ForkJoinPool. Це налаштування особливо каже щодо числа гарячих потоків, що пул виконує, щоб зменшити затримку обробки нового надходящого завдання. Ви можете прочитати більше щодо паралелізма в документації JDK ForkJoinPool.
Та ось інший приклад, що використовує "thread-pool-executor":
- my-thread-pool-dispatcher {
- # Dispatcher є ім'ям базованого на подіях диспечера
- type = Dispatcher
- # Який тип ExecutionService використовувати
- executor = "thread-pool-executor"
- # Конфігурація для пула потоків
- thread-pool-executor {
- # Мінімальне число потоків, щоб покрити число паралелізма на основі множника
- core-pool-size-min = 2
- # Число потоків ядра ... ceil(available processors * factor)
- core-pool-size-factor = 2.0
- # Мінімальне число потоків, щоб покрити число паралелізма на основі множника
- core-pool-size-max = 10
- }
- # Пропускна спроможність максимальне число повідомлень, що будуть оброблятись
- # на кожного актора, перед тим, як потік перестрибне на нового актора.
- # Встановіть 1 для максимальнї чесності.
- throughput = 100
- }
Щодо інших опцій дивіться розділ default-dispatcher в Конфігурації
Потім ви створюєте актора як звичайно, та визначаєте диспечера в конфігурації розгортання.
- import akka.actor.Props
- val myActor = context.actorOf(Props[MyActor], "myactor")
- akka.actor.deployment {
- /myactor {
- dispatcher = my-dispatcher
- }
- }
Альтернативою до конфігурації розгортання є визначення диспечера до кода. Якщо ви визначаєте dispatcher в конфігурації розгортання, тоді це значення буде використане, замість параметра, встановленого програмно.
- import akka.actor.Props
- val myActor =
- context.actorOf(Props[MyActor].withDispatcher("my-dispatcher"), "myactor1")
Зауваження
Диспечер, що ви задаєте в withDispatcher та властивість dispatcher в конфігурації розгортання, фактично є шляхом в вашій конфігурації . Так що в цьому прикладі це розділ вищого рівня, але ви можете, наприклад, покласти його як суб-секцію, де ви використовуєте крапки для позначення суб-секцій, ось так: "foo.bar.my-dispatcher"
Типи диспечерів
Є три різні типи диспечерів повідомлень:
Dispatcher
Диспечер на основі подій, що прикріплює набір акторів до пула потоків. Це диспечер по замовчанню, використовується якщо не вказано інше.
Сумісне використання: Необмежене
Поштові скриньки: Любі, створює одну для кожного актора
Приклади використання: По замовченню, Bulkheading
- Рушій:
java.util.concurrent.ExecutorService вказується з використанням "executor" з використанням "fork-join-executor", "thread-pool-executor" або FQCN akka.dispatcher.ExecutorServiceConfigurator
PinnedDispatcher
Цей диспечер виділяє унікальний потік для кожного актора, що використовує його; тобто, кожний актор буде мати свій власний пул потоків з тільки одним потоком в пулі.
Сумісне використання: Ні
Поштові скриньки: Любі, створює одну на актора
Приклади використання: Bulkheading
- Рушій: Любий
akka.dispatch.ThreadPoolExecutorConfigurator по замовчанню "thread-pool-executor"
BalancingDispatcher
Цей диспечер базується на екзекуторі та рушівний подіями, що буде намагатись перерозпреділити роботу з навантажених акторів на простоюючих.
Всі актори поділяють єдиний Mailbox, з якого отримують свої повідомлення.
Мається на увазі, що всі актори, що використовують один примірник цього диспечера, можуть обробляти всі повідомлення, що були надіслані одному актору; тобто, актори належать до пула акторів, та клієнту не гарантується, який саме примірник актора насправді обробляє окреме повідомлення.
Сумісне використання: Тільки актори одного типу
Поштові скриньки: Любі, створює один для всіх акторів
Приклади використання: Розподілення роботи
- Рушій:
java.util.concurrent.ExecutorService - вказується з використанням "executor" з використанням "fork-join-executor", "thread-pool-executor" або FQCN
akka.dispatcher.ExecutorServiceConfigurator
Зауважте, що ви не можете використовувати BalancingDispatcher як Диспечер маршрутизації (однак ви можете використовувати його для Routees).
CallingThreadDispatcher
- Цей диспечер виконує виклики тільки на поточному потоці. Цей диспечер не створює жодних нових потоків, але він може бути використаний з різних потоків одночасно для деякого актора. Дивіться CallingThreadDispatcher щодо деталей та обмежень.
- Сумісне використання: Необмежене
- Поштові скриньки: Любі, створює одну для актора на потік (на вимогу)
- Приклади використання: Тестування
- Рушій: Викликаючий потік (не дивно)
Більше прикладів конфігурації диспечера
Конфігурація диспечера з фіксованим розміром пула потоків, тобто, для акторів, що виконують блокуючий IO:
- blocking-io-dispatcher {
- type = Dispatcher
- executor = "thread-pool-executor"
- thread-pool-executor {
- fixed-pool-size = 32
- }
- throughput = 1
- }
Та потім використовуємо його:
- val myActor =
- context.actorOf(Props[MyActor].withDispatcher("blocking-io-dispatcher"), "myactor2")
Конфігурація PinnedDispatcher:
- my-pinned-dispatcher {
- executor = "thread-pool-executor"
- type = PinnedDispatcher
- }
Та потім використовуємо її:
- val myActor =
- context.actorOf(Props[MyActor].withDispatcher("my-pinned-dispatcher"), "myactor3")
Зауважте, що ця конфігурація thread-pool-executor для приклада вище my-thread-pool-dispatcher НЕ прийнятна. Це тому, що кожний актор матиме свій власний пул потоків при використанні PinnedDispatcher, та цей пул буде мати тікльки один потік.
Зауважте, що не гарантовано, що той же потік буде використовуватись з часом, оскільки використовується таймаут ядра пула для PinnedDispatcher, щоб утримувати використання ресурсів низьким в випадку простоя акторів. Щоб використовувати той же потік весь час, вам треба додати thread-pool-executor.allow-core-timeout=off до конфігурації PinnedDispatcher.
Поштові скриньки
В Akka Mailbox містить повідомлення, що призначені до Actor. Зазвичай, кожний Actor має свою власну скриньку, але, наприклад, в BalancingPool всі маршрути будуть поділяти один примірник поштової скриньки.
Вибір поштової скриньки
Запит типу черги повідомлень для актора
Можливо зробитит запити певний тип черги повідомлень для певного типа акторів, якщо цей актор розширятиме параметризований трейт RequiresMessageQueue. Ось приклад:
- import akka.dispatch.RequiresMessageQueue
- import akka.dispatch.BoundedMessageQueueSemantics
-
- class MyBoundedActor extends MyActor
- with RequiresMessageQueue[BoundedMessageQueueSemantics]
Параметр типа до трейта RequiresMessageQueue потребує бути відображеним на поштову скриньку в конфігурації, наступним чином:
- bounded-mailbox {
- mailbox-type = "akka.dispatch.BoundedMailbox"
- mailbox-capacity = 1000
- mailbox-push-timeout-time = 10s
- }
-
- akka.actor.mailbox.requirements {
- "akka.dispatch.BoundedMessageQueueSemantics" = bounded-mailbox
- }
Тепер кожного разу, коли ви створюєте актора з типом MyBoundedActor, він буде намагатись отримати обмежену поштову скриньку. Якщо актор має іншу скриньку, сконфігуровану для розгортання, або напряму, або через диспечера з вказаним типом скриньки, тоді це перевизначить це відображення.
Зауваження
Тип черги в поштовій скриньці, створеній для актора, буде перевірятись з запитаним типом в трейті, та якщо черга не реалізує потрібний тип, тоді створення актора зазнає невдачі.
Запит на тип черги повідомлень для диспечера
Диспечер також може мати запит на тип поштової скриньки для акторів, що роблять на ньому. Прикладом є BalancingDispatcher, що потребує чергу повідомлень, що потік-безпечні для декількох конкурентних споживачів. Така вимога формулюється в розділі конфігурації диспечера, таким чином:
- my-dispatcher {
- mailbox-requirement = org.example.MyInterface
- }
Отримана вимога називає клас або інтерфейс, що буде потім гарантованим супертипом реалізації черги повідомлень. Ви випадку конфлікта — тобто, якщо актор потребує тип скриньки, що не задовільняє цій вимозі — тоді створення актора зазнає невдачі.
Як обирається тип поштової скриньки
При стоворенні актора ActorRefProvider спочатку визначає диспечера, що буде його виконувати. Потім поштова скринька визначається наступним чином:
- Якщо конфігурація розгортання актора містить ключ
mailbox, тоді використовуються він вказує розділ конфігурації, що описує тип поштової скриньки, яка буде використовуватись. - Якщо
Props актора містить вибір поштової скриньки — тобто, на ній був викликаний withMailbox — тоді він вказує на розділ конфігурації, що описує тип поштової скриньки, яка буде використовуватись. - Якщо розділ конфігурації диспечера містить ключ
mailbox-type , той же розділ буде використано для конфігурації поштової скриньки. - Якщо актор потребує тип поштової скриньки, як описано вище, тоді для визначення потрібного типу поштової скриньки буде викостано відображення цієї вимоги; якщо це зазнає невдачі, тоді замість цього буде спробована вимога диспечера — якщо є.
- Якщо диспечер потребує тип поштової скриньки, як описано вище, тоді для визначення типу поштової скриньки буде використано відображення для цієї вимоги.
- Інакше буде використана скринька по замовчанню
akka.actor.default-mailbox.
Поштова скринька по замовчанню
Коли поштова скринька не вказана, як описано вище, використовується поштова скринька по замовчанню. По замовчанню це необмежена поштова скринька, на основі java.util.concurrent.ConcurrentLinkedQueue.
SingleConsumerOnlyUnboundedMailbox є навіть більш ефективною скринькою, та вона може бути використовуватись як скринька по замовчанню, але вона не може бути використана з BalancingDispatcher.
Конфігуарція SingleConsumerOnlyUnboundedMailbox як скриньки по замовчанню:
- akka.actor.default-mailbox {
- mailbox-type = "akka.dispatch.SingleConsumerOnlyUnboundedMailbox"
- }
Яка конфігурація до типу поштової скриньки
Кожна поштова скринька реалізована класом, що розширює MailboxType , та приймає два аргументи конструктора: об'єкт ActorSystem.Settings та розділаConfig. Останній обчислюється через отримання поіменованого розділу конфігурації від конфігурації системи акторів, перевизначаючи його ключ id на шлях конфігурацїі типу поштової скриньки, та додаючи відкат до розділу конфігурації поштової скриньки по замовчанню.
Вбудовані реалізації поштової скриньки
Akka іде з декількома реалізаціями поштової скриньки:
UnboundedMailbox (по замовчанню)
- Поштова скринька по замовчанню
- На основі
java.util.concurrent.ConcurrentLinkedQueue - Блокування: Ні
- Обмеження: Ні
- Ім'я конфігурації:
"unbounded" or "akka.dispatch.UnboundedMailbox"
SingleConsumerOnlyUnboundedMailbox
Ця черга може бути, а може не бути швидшою, ніж по замовчанню, в залежності від вашого випадку використання — переконайтесь, що перевірили за допомогою вимірів!
- На основі Multiple-Producer Single-Consumer queue, cannot be used with
BalancingDispatcher - Блокування: Ні
- Обмеження: Ні
- Ім'я конфігурації:
"akka.dispatch.SingleConsumerOnlyUnboundedMailbox"
NonBlockingBoundedMailbox
- На основі дуже ефективної черги багато-продьюсерів-один-споживач
- Блокування: Ні (discards overflowing messages into deadLetters)
- Обмеження: Так
- Ім'я конфігурації:
"akka.dispatch.NonBlockingBoundedMailbox"
UnboundedControlAwareMailbox
- Доставляє повідомлення, що розширює
akka.dispatch.ControlMessage з вищим преоритетом - На основі двох
java.util.concurrent.ConcurrentLinkedQueue - Блокування: Ні
- Обмеження: Ні
- Ім'я конфігурації: "akka.dispatch.UnboundedControlAwareMailbox"
UnboundedPriorityMailbox
- На основі
java.util.concurrent.PriorityBlockingQueue - Порядок доставлення однакового преоритета невизначене - на відміну від UnboundedStablePriorityMailbox
- Блокування: Ні
- Обмеження: Ні
- Ім'я конфігурації: "akka.dispatch.UnboundedPriorityMailbox"
UnboundedStablePriorityMailbox
- На основі
java.util.concurrent.PriorityBlockingQueue, огорнутий в akka.util.PriorityQueueStabilizer - Порядок FIFO збергається для повідомлень з однаковим преоритетом - на відміну від UnboundedPriorityMailbox
- Блокування: Ні
- Обмеження: Ні
- Ім'я конфігурації: "akka.dispatch.UnboundedStablePriorityMailbox"
Інші реалізації обмежених поштових скриньок, що будуть блокувати надсилача, якщо досягнута ємність, та сконфігуровано з ненульовим mailbox-push-timeout-time.
Зауваження
Наступні поштові скриньки повинні використовуватись з нульовим mailbox-push-timeout-time.
- BoundedMailbox
- На основі
java.util.concurrent.LinkedBlockingQueue - Блокування: Так, якщо використвується з ненульовим
mailbox-push-timeout-time, інакше Ні - Обмеження: Так
- Ім'я конфігурації: "bounded" або "akka.dispatch.BoundedMailbox"
- BoundedPriorityMailbox
- На основі
java.util.PriorityQueue, огорнутого в akka.util.BoundedBlockingQueue - Порядок доставлення для повідомлень з однаковим преоритетом не визначений - на відміну від
BoundedStablePriorityMailbox - Блокування: Так, якщо використвується з ненульовим
mailbox-push-timeout-time, інакше Ні - Обмеження: Так
- Ім'я конфігурації:
"akka.dispatch.BoundedPriorityMailbox"
- BoundedStablePriorityMailbox
- На основі
java.util.PriorityQueue, огорнутого в akka.util.PriorityQueueStabilizer та akka.util.BoundedBlockingQueue - Порядок FIFO зберігається для повідомлень з однаковим преоритетом - на відміну від BoundedPriorityMailbox
- Блокування: Так, якщо використвується з ненульовим
mailbox-push-timeout-time, інакше Ні - Обмеження: Так
- Ім'я конфігурації: "akka.dispatch.BoundedStablePriorityMailbox"
- BoundedControlAwareMailbox
- Доставляє повідомлення, що розширюють
akka.dispatch.ControlMessage з вищим преоритетом - На основі двох
java.util.concurrent.ConcurrentLinkedQueue та блокується на постановці в чергу, якщо досягнута ємність - Блокування: Так, якщо використвується з ненульовим
mailbox-push-timeout-time, інакше Ні - Обмеження: Так
- Ім'я конфігурації: "akka.dispatch.BoundedControlAwareMailbox"
Приклади конфігурації поштових скриньок
PriorityMailbox
Як створити PriorityMailbox:
- import akka.dispatch.PriorityGenerator
- import akka.dispatch.UnboundedStablePriorityMailbox
- import com.typesafe.config.Config
-
- // В цьому випадку ми наслідуємо від UnboundedStablePriorityMailbox
- // та запускаємо його з генератором преоритетов
- class MyPrioMailbox(settings: ActorSystem.Settings, config: Config)
- extends UnboundedStablePriorityMailbox(
- // Створюємо новий PriorityGenerator, нижчий prio означає біль важливе
- PriorityGenerator {
- // Повідомлення 'highpriority повинно трактувати першим,якщо можливо
- case 'highpriority => 0
-
- // Повідомлення 'lowpriority повинно трактувати першим,якщо можливо
- case 'lowpriority => 2
-
- // PoisonPill, якщо немає інших
- case PoisonPill => 3
-
- // Ми ставимо 1 по замовчанню, що між високим та низким
- case otherwise => 1
- })
Та потім додаємо до конфігурації:
- prio-dispatcher {
- mailbox-type = "docs.dispatcher.DispatcherDocSpec$MyPrioMailbox"
- // Інша конфігуарція диспечера знаходиться тут
- }
Та ось приклад, як ви будете використовувати це:
- // Створюємо нового актора, що тільки друкує те, що обробляє
- class Logger extends Actor {
- val log: LoggingAdapter = Logging(context.system, this)
-
- self ! 'lowpriority
- self ! 'lowpriority
- self ! 'highpriority
- self ! 'pigdog
- self ! 'pigdog2
- self ! 'pigdog3
- self ! 'highpriority
- self ! PoisonPill
-
- def receive = {
- case x => log.info(x.toString)
- }
- }
- val a = system.actorOf(Props(classOf[Logger], this).withDispatcher(
- "prio-dispatcher"))
-
- /*
- * Logs:
- * 'highpriority
- * 'highpriority
- * 'pigdog
- * 'pigdog2
- * 'pigdog3
- * 'lowpriority
- * 'lowpriority
- */
Також можливо сконфігурувати тип поштової скриньки напряму, таким чином:
- prio-mailbox {
- mailbox-type = "docs.dispatcher.DispatcherDocSpec$MyPrioMailbox"
- // Інша конфігурація поштової скриньки знаходиться тут
- }
-
- akka.actor.deployment {
- /priomailboxactor {
- mailbox = prio-mailbox
- }
- }
Та потім використовуємо її, або з розгортання, як тут:
- import akka.actor.Props
- val myActor = context.actorOf(Props[MyActor], "priomailboxactor")
або з кода:
- import akka.actor.Props
- val myActor = context.actorOf(Props[MyActor].withMailbox("prio-mailbox"))
ControlAwareMailbox
ControlAwareMailbox може бути дуже корисним, якщо актор потребує можливості отримувати контрольні повідомлення безпосередньо, не важливо, скільки інших повідомлень вже в поштовій скриньці.
Це може бути сконфігуровано таким чином:
- control-aware-dispatcher {
- mailbox-type = "akka.dispatch.UnboundedControlAwareMailbox"
- // Інша конфігурація диспечера знаходиться тут
- }
Контрольні повідомлення мають розширювати трейт ControlMessage:
- import akka.dispatch.ControlMessage
-
- case object MyControlMessage extends ControlMessage
Та ось приклад як ви можете використовувати це:
- // Ми створюємо нового актора, що лише роздруковує те, що обробляє
- class Logger extends Actor {
- val log: LoggingAdapter = Logging(context.system, this)
-
- self ! 'foo
- self ! 'bar
- self ! MyControlMessage
- self ! PoisonPill
-
- def receive = {
- case x => log.info(x.toString)
- }
- }
- val a = system.actorOf(Props(classOf[Logger], this).withDispatcher(
- "control-aware-dispatcher"))
-
- /*
- * Logs:
- * MyControlMessage
- * 'foo
- * 'bar
- */
Створення вашого власного типу поштової скриньки
Приклад вартий тисячі балачок:
- import akka.actor.ActorRef
- import akka.actor.ActorSystem
- import akka.dispatch.Envelope
- import akka.dispatch.MailboxType
- import akka.dispatch.MessageQueue
- import akka.dispatch.ProducesMessageQueue
- import com.typesafe.config.Config
- import java.util.concurrent.ConcurrentLinkedQueue
- import scala.Option
-
- // Маркерний трейт, використаний для відображення вимог скриньки
- trait MyUnboundedMessageQueueSemantics
-
- object MyUnboundedMailbox {
- // Реалізація MessageQueue
- class MyMessageQueue extends MessageQueue
- with MyUnboundedMessageQueueSemantics {
-
- private final val queue = new ConcurrentLinkedQueue[Envelope]()
-
- // це має бути реалізоване; черга використана як приклад
- def enqueue(receiver: ActorRef, handle: Envelope): Unit =
- queue.offer(handle)
- def dequeue(): Envelope = queue.poll()
- def numberOfMessages: Int = queue.size
- def hasMessages: Boolean = !queue.isEmpty
- def cleanUp(owner: ActorRef, deadLetters: MessageQueue) {
- while (hasMessages) {
- deadLetters.enqueue(owner, dequeue())
- }
- }
- }
- }
-
- // Це реалізація Mailbox
- class MyUnboundedMailbox extends MailboxType
- with ProducesMessageQueue[MyUnboundedMailbox.MyMessageQueue] {
-
- import MyUnboundedMailbox._
-
- // Цей структура конструктора має існувати, вона викликається Akka
- def this(settings: ActorSystem.Settings, config: Config) = {
- // розташуйте тут ваш код ініціалізації
- this()
- }
-
- // Метод create викликається для створення MessageQueue
- final override def create(
- owner: Option[ActorRef],
- system: Option[ActorSystem]): MessageQueue =
- new MyMessageQueue()
- }
Та потім ви просто вказуєте FQCN до вашого MailboxType, як значення "mailbox-type" в конфігурації диспечера, або в конфігурації поштової скриньки.
Зауваження
Переконайтесь, що включили конструктор, що приймає аргументи akka.actor.ActorSystem.Settings та com.typesafe.config.Config, бо цей конструктор викликається рефлекційно для конструювання вашого типу поштової скриньки. Кофігурація, передана як другий аргумент, є тим розділом конфігурації, що описує налаштування диспечера або скриньки з використанням цього типу скриньки; тип скриньки буде утворений по разу для кожного диспечера або скриньки, що встановлюється з його використанням.
Ви також можете використовувати поштову скриньку як вимогу до диспечера, ось так:
- custom-dispatcher {
- mailbox-requirement =
- "docs.dispatcher.MyUnboundedJMessageQueueSemantics"
- }
-
- akka.actor.mailbox.requirements {
- "docs.dispatcher.MyUnboundedJMessageQueueSemantics" =
- custom-dispatcher-mailbox
- }
-
- custom-dispatcher-mailbox {
- mailbox-type = "docs.dispatcher.MyUnboundedJMailbox"
- }
Або визначаючи вимогу на вашому класі актора, ось так:
- class MySpecialActor extends Actor
- with RequiresMessageQueue[MyUnboundedMessageQueueSemantics] {
- // ...
- }
Спеціальна семантика system.actorOf
Щоб зробити system.actorOf одночасно синхронним та неблокуючим, та при тому підтримувати тип результата ActorRef (та семантику, коли повернутий ref повністю функціональний), для цього випадка має місце особлива обробка. Поза сценою створюється порожній тип посилання на актора, що надсилається системному актору-охоронцю, що насправді створює актора та його контекст, та покладає це в посилання. Докі це не відбудеться, повідомлення, надіслані на ActorRef, будуть локально поставлені в чергу, та тільки після заміни реального заповнення вони будуть передані в реальну поштову скриньку. Таким чином,
- val props: Props = ...
- // цей актор використовує MyCustomMailbox, що, за очікуванням, є синглтоном
- system.actorOf(props.withDispatcher("myCustomMailbox")) ! "bang"
- assert(MyCustomMailbox.instance.getLastEnqueuedMessage == "bang")
це, напевне, схибить; вам буде потрібно надати деякий час, щоб передати та повторити перевірку, в дусі TestKit.awaitCond.
Маршрутизація
Повідомлення можуть бути надіслані через маршрутизатор, щоб ефективно передати їх до цільових акторів, відомих як його призначення. Router може бути використаний в середині або ззовні актора, та ви можете керувати призначеннями власноруч, або використовувати самодостатній актор-маршрутизатор з можливостями конфігурації.
Можуть бути використані різні стратегії, відповідно до потреб вашого застосування. Akka іде з декількома корисними стратегіями маршрутизації прямо з коробки. Але, як ви побачите в цій главі, також можливо створити вашу власну.
Простий маршрутизатор
Наступний приклад ілюструє, як використовувати Router та керувати призначеннями з актора.
- import akka.routing.{ ActorRefRoutee, RoundRobinRoutingLogic, Router }
-
- class Master extends Actor {
- var router = {
- val routees = Vector.fill(5) {
- val r = context.actorOf(Props[Worker])
- context watch r
- ActorRefRoutee(r)
- }
- Router(RoundRobinRoutingLogic(), routees)
- }
-
- def receive = {
- case w: Work =>
- router.route(w, sender())
- case Terminated(a) =>
- router = router.removeRoutee(a)
- val r = context.actorOf(Props[Worker])
- context watch r
- router = router.addRoutee(r)
- }
- }
Ми створили Router та вказали, що він повинен використовувати RoundRobinRoutingLogic при маршрутизації до пунктів призначення.
Логіка маршрутизації, що надходить з Akka, така:
akka.routing.RoundRobinRoutingLogicakka.routing.RandomRoutingLogicakka.routing.SmallestMailboxRoutingLogicakka.routing.BroadcastRoutingLogicakka.routing.ScatterGatherFirstCompletedRoutingLogicakka.routing.TailChoppingRoutingLogicakka.routing.ConsistentHashingRoutingLogic
Ми сворили призначення як звичайні діти-актори, огорнуті в ActorRefRoutee. Ми наглядаємо за призаченнями, щоб бути в змозі замінити їх, якщо вони завершаться.
Надсилання повідомлень через маршрутизатор робиться через метод route, як це робиться для повідомлень Work в прикладі вище.
Router є незмінним та RoutingLogic стійка до потоків; це означає, що також можуть використовуватись за межами акторів.
Зауваження
Загалом кожне повідомлення, надіслане до маршрутизатора, буде надіслане до його призначень, але є одне виключення. Особливі Широкополосні повідомлення будуть адіслані до всіх пунктів призначення
Актор-маршрутизатор
Маршрутизатор також може бути створений як самодостатній актор, що керує призначеннями, на завантажує логіку маршрутизації та інші налаштування з конфігурації.
Цей тип актора-маршрутизатора іде в двох різних варіантах:
- Pool - Маршрутизатор створює призначення як дочірні актори, та видаляє їх з маршрутизації, коли вони завершуються.
- Group - Актори призначення створюються ззовні до маршрутизатора, та маршрутизатор надсилає повідомлення по вказаному шляху з використанням селекторів акторів, без нагляду за завершенням.
Налаштування для актора-маршрутизатора можуть бути визначені в конфігурації або програмно. Хоча актори-маршрутизатори можуть бути визначені в файлі конфігурації, вони все ще мають створюватись програмно, тобто, ви не можете зробити маршрутизатор тільки через зовнішній файл. Якщо ви визначите актора-маршрутизатор в файлі конфігурації, тоді ці налаштування будуть використані замість любих програмно наданих параметрів.
Ви надсилаєте повідомлення до призначень через актор-маршрутизатор в той же спосіб, як і для звичайних асторів, тобто через його ActorRef. Актори-маршрутизатори пересилають повідомлення до призначень без зміни первинного надсилача. Коли призначення відповідає на маршрутизоване повідомлення, відповідь буде доставлена первинному надсилачу, та не до актора-маршрутизатора.
Зауваження
Загалом, любе повідомлення, надіслане до маршрутизатора, буде надіслане до його призначень, але є декілька виключень. Вони задокументовані в розділі нижче, Спеціальна обробка повідомлень.
Пул
Наступний код та клаптики конфігурації показують, як створити круговий (round-robin) маршрутизатор, що пересилає повідомлення до п'яти робочих призначень Worker. Призначення будуть створені як діти маршрутизатора.
- akka.actor.deployment {
- /parent/router1 {
- router = round-robin-pool
- nr-of-instances = 5
- }
- }
- val router1: ActorRef =
- context.actorOf(FromConfig.props(Props[Worker]), "router1")
А ось той же приклад, але з налаштуванням маршрутизатора, що надається програмно, замість походити з конфігурації.
- val router2: ActorRef =
- context.actorOf(RoundRobinPool(5).props(Props[Worker]), "router2")
Призначення з віддаленим розташуванням
На додаток до змоги створювати локальні актори в якості пунктів призначення, ви можете проінструктувати маршрутизатор розташувати створених їм дітей на наборі віддалених вузлів. Призначення будуть розгорнуті в стилі по-колу. Щоб розмістити призначення віддалено, огорніть конфігурацію маршрутизатора в RemoteRouterConfig, додаючи віддалені адреси вузлів до розташування. Віддалене розгортання потребує включення в classpath модуля akka-remote.
- import akka.actor.{ Address, AddressFromURIString }
- import akka.remote.routing.RemoteRouterConfig
- val addresses = Seq(
- Address("akka.tcp", "remotesys", "otherhost", 1234),
- AddressFromURIString("akka.tcp://othersys@anotherhost:1234"))
- val routerRemote = system.actorOf(
- RemoteRouterConfig(RoundRobinPool(5), addresses).props(Props[Echo]))
Надсилачі
По замовчанню, коли актор-призначення надсилає повідомлення, він буде неявно вказувати себе як надсилача.
- sender() ! x // відповіді будуть надходити до цього актора
Однак, також часто корисно для призначень вказувати маршрутизатор як надсилача. Наприклад, ви можете побажати встановити маршрутизатор як надсилача, якщо ви бажаєте приховати деталі призначень за маршрутизатором. Наступний код показує, як встановити батьківський маршрутизатор в якості надсилача.
- sender().tell("reply", context.parent) // відповіді надійдуть до батька
- sender().!("reply")(context.parent) // альтернативний синтаксис (зауважте дужки!)
Нагляд
Призначення, що створені маршрутизатором пула, будуть створені як діти маршрутизатора. Таким чином, маршрутизатор є супервізором дітей.
Стратегія супервізора актора-маршрутизатора може бути сконфігурована з допомогою властивості supervisorStrategy для Pool. Якщо конфігурація не запроваджена, маршрутизатор по замовчанню обирає стратегію “завжди ескалувати”. Це означає, що помилки передаються на обробку догори, до супервізора маршрутизатора. Супервізор маршрутизатора буде вирішувати, що робити з ціма помилками.
Зауважте, що супервізор маршрутизатора буде розглядати помилки як помилки самого маршрутизатора. Таким чином, вказівка зупинити або рестартувати спричинить зупинку або рестарт самого маршрутизатора. Маршрутизатор, зі свого боку, викличе зупинку або рестарт своїх дітей.
Треба зазначити, що поведінка рестарта маршрутизатора була перекрита, так що рестарт, хоча все ще пере-створює дітей, буде зберігати ту ж кількість акторів в пулі.
Це означає, що якщо ви не вказали в supervisorStrategy маршрутизатора або його батька, збій в призначенні буде ескалувати до батька маршрутизатоа, що буде по замовчанню рестартувати маршрутизатор, що також рестартує всі призначення (використовується Escalate, та не зупиняє призначення під час рестарта). Причина зробити поведінку по замовчанню саме такою в тому, що додавання withRouter до визначення дитини не змінює стратегію нагляду, застосовану для дитини. Це може бути неефективністю, що ви можете уникнути, задавши стратегію при визначенні маршрутизатора.
Завдання стратегії робиться просто:
- val escalator = OneForOneStrategy() {
- case e ⇒ testActor ! e; SupervisorStrategy.Escalate
- }
- val router = system.actorOf(RoundRobinPool(1, supervisorStrategy = escalator).props(
- routeeProps = Props[TestActor]))
Зауваження
Якщо дитина кругового маршрутизатора завершуєтсья, круговий маршрутизатор не буде відгалужувати нову дитину. В випадку, коли всі діти маршрутизатора пула завершаться, завершиться і самий маршрутизатор, якщо це тільки не динамічний маршрутизатор, що використовує ресайзер.
Група
Іноді, замість мати актора-маршрутизатор, що створює свої призначення, бажано створити призначення окремо, та запровадити їх до маршрутизатора для використання. Ви можете зробити це, передаючи шляхи призначень до конфігурації маршрутизатора. Повідомлення будуть надіслані за допомогою ActorSelection по цім шляхам.
Приклад нижче показує, як створити маршрутизатор, провадячи йому рядки шляхів трьох акторів-призначень.
- akka.actor.deployment {
- /parent/router3 {
- router = round-robin-group
- routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"]
- }
- }
- val router3: ActorRef =
- context.actorOf(FromConfig.props(), "router3")
Ось той же приклад, але конфігурація маршрутизатора запроваджена програмно, замість конфігурації.
- val router4: ActorRef =
- context.actorOf(RoundRobinGroup(paths).props(), "router4")
Актори-призначення створені ззовні відносно маршрутизатора:
- system.actorOf(Props[Workers], "workers")
- class Workers extends Actor {
- context.actorOf(Props[Worker], name = "w1")
- context.actorOf(Props[Worker], name = "w2")
- context.actorOf(Props[Worker], name = "w3")
- // ...
Шлях може містити інформацію про протокол та адресу акторів, що роблять на віддалених вузлах. Віддалені зв'язки потребують модуля akka-remote, включеного в classpath.
- akka.actor.deployment {
- /parent/remoteGroup {
- router = round-robin-group
- routees.paths = [
- "akka.tcp://app@10.0.0.1:2552/user/workers/w1",
- "akka.tcp://app@10.0.0.2:2552/user/workers/w1",
- "akka.tcp://app@10.0.0.3:2552/user/workers/w1"]
- }
- }
Використання маршрутизатора
В цьому розділі ми опишемо, як створити різні типи акторів-маршрутизаторів.
Актор-маршрутизатор в цьому розділі створений в акторі вищого рівня на ім'я parent. Зауважте, що шлях розгортання в конфігурації починається з /parent/, за яким слідує ім'я актора-маршрутизатора.
- system.actorOf(Props[Parent], "parent")
RoundRobinPool та RoundRobinGroup
Маршрутизує по колу до своїх призначень.
RoundRobinPool, визначений в конфігурації:
- akka.actor.deployment {
- /parent/router1 {
- router = round-robin-pool
- nr-of-instances = 5
- }
- }
- val router1: ActorRef =
- context.actorOf(FromConfig.props(Props[Worker]), "router1")
RoundRobinPool, визначений в коді:
- val router2: ActorRef =
- context.actorOf(RoundRobinPool(5).props(Props[Worker]), "router2")
RoundRobinGroup, визначений в конфігурації:
- akka.actor.deployment {
- /parent/router3 {
- router = round-robin-group
- routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"]
- }
- }
- val router3: ActorRef =
- context.actorOf(FromConfig.props(), "router3")
RoundRobinGroup, визначений в коді:
- val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3")
- val router4: ActorRef =
- context.actorOf(RoundRobinGroup(paths).props(), "router4")
RandomPool та RandomGroup
Цей тип маршрутизатора обирає одного зі своїх призначень навмання для кожного повідомлення.
RandomPool, визначений в конфігурації:
- akka.actor.deployment {
- /parent/router5 {
- router = random-pool
- nr-of-instances = 5
- }
- }
- val router5: ActorRef =
- context.actorOf(FromConfig.props(Props[Worker]), "router5")
RandomPool, визначений в коді:
- val router6: ActorRef =
- context.actorOf(RandomPool(5).props(Props[Worker]), "router6")
RandomGroup, визначений в конфігурації:
- akka.actor.deployment {
- /parent/router7 {
- router = random-group
- routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"]
- }
- }
- val router7: ActorRef =
- context.actorOf(FromConfig.props(), "router7")
RandomGroup, визначений в коді:
- val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3")
- val router8: ActorRef =
- context.actorOf(RandomGroup(paths).props(), "router8")
BalancingPool
Маршрутизатор, що буде намагатись перерозподілити роботу з навантажених на простоюючі призначення. Всі маршрути поділяють єдину поштову скриньку.
Зауваження
BalancingPool має таку властивість, що всі його призначення не мають справжньої різної ідентичності: вони мають різні назви, але розмова з ними не буде закінчуватись на вірному акторі в більшості випадків. Таким чином, ви не можете використовувати його для потоків виконання, що потребують зберігання стану на призначенні, ви маєте вкладати весь стан в повідомлення.
З допомогою SmallestMailboxPool ви можете мати вертикально маштабований сервіс, що може взаємодіяти в стилі стану з іншими сервісами бек-енду, перед тим, як відповісти оригінальному клієнту. Інша перевага в тому, що він не накладає обмеження на реалізацію черги повідомлень, як робить BalancingPool.
BalancingPool, визначений в конфігурації:
- akka.actor.deployment {
- /parent/router9 {
- router = balancing-pool
- nr-of-instances = 5
- }
- }
- val router9: ActorRef =
- context.actorOf(FromConfig.props(Props[Worker]), "router9")
BalancingPool, визначений в коді:
- val router10: ActorRef =
- context.actorOf(BalancingPool(5).props(Props[Worker]), "router10")
Додаткова конфігурація для балансуючого диспечера, що використовується пулом, може бути сконфігурована в розділі pool-dispatcher конфігурації розгортання маршрутизатора.
- akka.actor.deployment {
- /parent/router9b {
- router = balancing-pool
- nr-of-instances = 5
- pool-dispatcher {
- attempt-teamwork = off
- }
- }
- }
BalancingPool автоматично використовує спеціальний BalancingDispatcher для своїх призначень - незважаючи на жодний диспечер, що встановлений на об'єкті Props призначення. Це потрібно, щоб реалізувати семантику балансування через розділення однієї поштової скриньки для всіх призначень.
Хоча неможливо змінити диспечер, що використовуєть призначеннями, можливо гарно налаштувати використовуваний виконавець. по замовчанню використовується fork-join-dispatcher, та може бути сконфігурований, як пояснено в Диспечерах. В ситуаціях, коли призначення очікувано будуть виконувати блокуючі операції, може бути корисним замінити його на thread-pool-executor, явно підказуючи число розміщених потоків:
- akka.actor.deployment {
- /parent/router10b {
- router = balancing-pool
- nr-of-instances = 5
- pool-dispatcher {
- executor = "thread-pool-executor"
-
- # розмістити рівно 5 потоків для цього пула
- thread-pool-executor {
- core-pool-size-min = 5
- core-pool-size-max = 5
- }
- }
- }
- }
Немає варіанту Group для BalancingPool.
SmallestMailboxPool
Маршрутизатор, що намагається надсилати до не-призупиненого дитячого призначення з найменшою кількістю повідомлень в скриньці. Вибір робиться в такому порядку:
- береться любе простоююче призначення (що не обробляє повідомлення) з порожньою скринькою
- береться любе пирзначення з порожньою скринькою
- береться призначення з найменьшими підвислими повідомленнями в скриньці
- береться любе віддалене призначення, віддалені актори мають найнижчий приоритет, оскільки розмір їх скриньки невідомий
SmallestMailboxPool, визначений в конфігурації:
- akka.actor.deployment {
- /parent/router11 {
- router = smallest-mailbox-pool
- nr-of-instances = 5
- }
- }
- val router11: ActorRef =
- context.actorOf(FromConfig.props(Props[Worker]), "router11")
SmallestMailboxPool, визначений в коді:
- val router12: ActorRef =
- context.actorOf(SmallestMailboxPool(5).props(Props[Worker]), "router12")
Немає варіанту Group для SmallestMailboxPool, оскільки розмір скриньки та внутрішній стан диспечеризації актора на практиці недоступний для шляхів призначень.
BroadcastPool та BroadcastGroup
Широкополосний маршрутизатор пересилає повідомлення, що отримує, до всіх призначень.
BroadcastPool, визначений в конфігурації:
- akka.actor.deployment {
- /parent/router13 {
- router = broadcast-pool
- nr-of-instances = 5
- }
- }
- val router13: ActorRef =
- context.actorOf(FromConfig.props(Props[Worker]), "router13")
BroadcastPool, визначений в коді:
- val router14: ActorRef =
- context.actorOf(BroadcastPool(5).props(Props[Worker]), "router14")
BroadcastGroup, визначений в конфігурації:
- akka.actor.deployment {
- /parent/router15 {
- router = broadcast-group
- routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"]
- }
- }
- val router15: ActorRef =
- context.actorOf(FromConfig.props(), "router15")
BroadcastGroup, визначений в коді:
- val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3")
- val router16: ActorRef =
- context.actorOf(BroadcastGroup(paths).props(), "router16")
Зауваження
Широкополосні маршрутизатори завжди розповсюджують кожне повідомлення до своїх призначень. Якщо ви не бажаєте пересилати любе повідомлення, тоді ви можете використовувати не-широкополосний маршрутизатор, та використовувати Broadcast Messages за необхідністю.
ScatterGatherFirstCompletedPool та ScatterGatherFirstCompletedGroup
ScatterGatherFirstCompletedRouter буде надсилати повідомлення до всіх своїх пунктів призначень. Потім він очікує першу зворотню відповідь. Цей результат буде надісланий назад до оригінального надсилача. Інші відповіді будуть відкинуті.
Очікується щонайменьше одна відповідь в сконфігурований проміжок часу, інакше буде відповідь akka.pattern.AskTimeoutException в akka.actor.Status.Failure.
ScatterGatherFirstCompletedPool, визначений в конфігурації:
- akka.actor.deployment {
- /parent/router17 {
- router = scatter-gather-pool
- nr-of-instances = 5
- within = 10 seconds
- }
- }
- val router17: ActorRef =
- context.actorOf(FromConfig.props(Props[Worker]), "router17")
ScatterGatherFirstCompletedPool, визначений в коді:
- val router18: ActorRef =
- context.actorOf(ScatterGatherFirstCompletedPool(5, within = 10.seconds).
- props(Props[Worker]), "router18")
ScatterGatherFirstCompletedGroup, визначений в конфігурації:
- akka.actor.deployment {
- /parent/router19 {
- router = scatter-gather-group
- routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"]
- within = 10 seconds
- }
- }
- val router19: ActorRef =
- context.actorOf(FromConfig.props(), "router19")
ScatterGatherFirstCompletedGroup, визначений в коді:
- val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3")
- val router20: ActorRef =
- context.actorOf(ScatterGatherFirstCompletedGroup(
- paths,
- within = 10.seconds).props(), "router20")
TailChoppingPool та TailChoppingGroup
TailChoppingRouter буде з початку надсилати повідомлення до одного, випадково обраного, призначення, та потім, через невелику затримку, до другого призначення (випадково обраного серед залишку), і так далі. Він очікує першу зворотню відповідь, та пересилає її назад до оригінального надсилача. Інші відповіді будуть відкинуті.
Ціллю цього маршрутизатора є зменшити затримку через виконання надмірних запитів до багатьох призначень, вважаючи, що один з інших акторів може бути швидшим щодо відповіді, ніж перший.
Ця оптимізація була гарно описана в блог пості Пітером Байлі: Виконання надмірної роботи для прискорення розподілених запитів.
TailChoppingPool, визначений в конфігурації:
- akka.actor.deployment {
- /parent/router21 {
- router = tail-chopping-pool
- nr-of-instances = 5
- within = 10 seconds
- tail-chopping-router.interval = 20 milliseconds
- }
- }
- val router21: ActorRef =
- context.actorOf(FromConfig.props(Props[Worker]), "router21")
TailChoppingPool, визначений в коді:
- val router22: ActorRef =
- context.actorOf(TailChoppingPool(5, within = 10.seconds, interval = 20.millis).
- props(Props[Worker]), "router22")
TailChoppingGroup, визначений в конфігурації:
- akka.actor.deployment {
- /parent/router23 {
- router = tail-chopping-group
- routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"]
- within = 10 seconds
- tail-chopping-router.interval = 20 milliseconds
- }
- }
- val router23: ActorRef =
- context.actorOf(FromConfig.props(), "router23")
TailChoppingGroup, визначений в коді:
- val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3")
- val router24: ActorRef =
- context.actorOf(TailChoppingGroup(
- paths,
- within = 10.seconds, interval = 20.millis).props(), "router24")
ConsistentHashingPool та ConsistentHashingGroup
The ConsistentHashingPool використовує узгоджене хешування для обрання призначення на основі надісланого повідомлення. Ця глава дає гарний погляд на те, як реалізоване узгоджене хешування.
Є три шляхи для визначення того, які дані використовувати для узгодженого хеша.
- Ви можете визначити
hashMapping маршрутизатора для відображення надходячих повідомлень на їх узгоджені ключі хеша. Це робить рішення прозорим для надсилача. - Повідомлення можуть реалізовати
akka.routing.ConsistentHashingRouter.ConsistentHashable. Ключ є частиною повідомлення, та є зручним визначати його разом з визначенням повідомлення. - Повідомлення може бути огорнуте в
akka.routing.ConsistentHashingRouter.ConsistentHashableEnvelope для визначення, які дані використовувати для узгодженого ключа кеша. Надсилач знає ключ для використання.
Ці шляхи визначення ключа узгодженого хеша можуть використовуватись разом та одночасно для одного і того ж маршрутизатора. Першим перевіряється hashMapping.
Приклад кода:
- import akka.actor.Actor
- import akka.routing.ConsistentHashingRouter.ConsistentHashable
-
- class Cache extends Actor {
- var cache = Map.empty[String, String]
-
- def receive = {
- case Entry(key, value) => cache += (key -> value)
- case Get(key) => sender() ! cache.get(key)
- case Evict(key) => cache -= key
- }
- }
-
- final case class Evict(key: String)
-
- final case class Get(key: String) extends ConsistentHashable {
- override def consistentHashKey: Any = key
- }
-
- final case class Entry(key: String, value: String)
- import akka.actor.Props
- import akka.routing.ConsistentHashingPool
- import akka.routing.ConsistentHashingRouter.ConsistentHashMapping
- import akka.routing.ConsistentHashingRouter.ConsistentHashableEnvelope
-
- def hashMapping: ConsistentHashMapping = {
- case Evict(key) => key
- }
-
- val cache: ActorRef =
- context.actorOf(ConsistentHashingPool(10, hashMapping = hashMapping).
- props(Props[Cache]), name = "cache")
-
- cache ! ConsistentHashableEnvelope(
- message = Entry("hello", "HELLO"), hashKey = "hello")
- cache ! ConsistentHashableEnvelope(
- message = Entry("hi", "HI"), hashKey = "hi")
-
- cache ! Get("hello")
- expectMsg(Some("HELLO"))
-
- cache ! Get("hi")
- expectMsg(Some("HI"))
-
- cache ! Evict("hi")
- cache ! Get("hi")
- expectMsg(None)
В прикладі вище ви бачите, що повідомлення Get реалізує саме ConsistentHashable, докі повідомлення Entry огорнуте в ConsistentHashableEnvelope. Повідомлення Evict ообробляється через часткову функцію hashMapping.
ConsistentHashingPool, визначений в конфігурації:
- akka.actor.deployment {
- /parent/router25 {
- router = consistent-hashing-pool
- nr-of-instances = 5
- virtual-nodes-factor = 10
- }
- }
- val router25: ActorRef =
- context.actorOf(FromConfig.props(Props[Worker]), "router25")
ConsistentHashingPool, визначений в коді:
- val router26: ActorRef =
- context.actorOf(
- ConsistentHashingPool(5).props(Props[Worker]),
- "router26")
ConsistentHashingGroup, визначений в конфігурації:
- akka.actor.deployment {
- /parent/router27 {
- router = consistent-hashing-group
- routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"]
- virtual-nodes-factor = 10
- }
- }
- val router27: ActorRef =
- context.actorOf(FromConfig.props(), "router27")
ConsistentHashingGroup, визначений в коді:
- val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3")
- val router28: ActorRef =
- context.actorOf(ConsistentHashingGroup(paths).props(), "router28")
virtual-nodes-factor є числом віртуальних вузлів на одне призначення, що використовується в колі вузлів узгодженого хеша для підтримки більш рівномірного розподілу.
Повідомлення з особливою обробкою
Більшість повідомлень, що актори надсилають до маршрутизатора, будуть переслані згідно логіки маршрутизації. Але є декілька типів повідомлень, що мають особливу поведінку.
Зауважте, що це особливі повідомлення, за винятком повідомлення Broadcast, обробляються тільки актором, що є окремим маршрутизатором, але не компонентом akka.routing.Router, описаним в Простий маршрутизатор.
Широкополосні повідомлення
Повідомлення Broadcast може використовуватись для надсилання повідомлення всім призначенням маршрутизатора. Коли маршрутизатор отримує повідомлення Broadcast, він розішле закладку цього повідомлення до всіх призначень, не важливо, як цей маршрутизатор буде звичайно пересилати свої повідомлення.
Приклад нижче показує, як ви можете використовувати повідомлення Broadcast для надсилання дуже важливого повідомлення кожному пункту призначення маршрутизації.
- import akka.routing.Broadcast
- router ! Broadcast("Watch out for Davy Jones' locker")
В цьому прикладі маршрутизатор надсилає повідомлення Broadcast, виділяє його закладку ("Watch out for Davy Jones' locker"), та потім надсилає її до всіх призначень маршрутизатора. Це справа кожного актора обробити отриману закладку повідомлення.
Повідомлення PoisonPill
Повідомлення PoisonPill має спеціальну обробку для всіх акторів, включаючи маршрутизатори. Коли любий актор отримує повідомлення PoisonPill, цей актор буде зупинений. Дивіться документацію щодо PoisonPill для подробиць.
- import akka.actor.PoisonPill
- router ! PoisonPill
Для маршрутизатора, що звичайно передає повідомлення до призначень, важливо уявляти, що повідомлення PoisonPill обробляються тільки маршрутизатором. Повідомлення PoisonPill, надіслані до маршрутизатора, не будуть переслані до призначень.
Однак повідомлення PoisonPill, надіслане маршрутизатору, все ще може впливати на призначення, оскільки воно буде зупиняти маршрутизатор, та при зупинці маршрутизатора він також зупинить своїх дітей. Зупинка дітей є звичайною поведінкою актора. Маршрутизатор буде зупиняти призначення, що він сворив як своїх дітей. Кожне дитя буде обробляти свої поточні повідомлення, та потім зупинені. Це може призвести до того, що деякі повідомлення не будуть оброблені. Дивіться документацію щодо Зупинки акторів для додаткової інформації.
Якщо ви бажаєте зупинити маршрутизатор та його призначення, але ви бужаєте спершу обробити всі повідомлення, що наразі в скриньках призначень, тоді ви не повинні надсилати повідомлення PoisonPill маршрутизатору. Замість ви повинні огорнути повідомлення PoisonPill всередині повідомлення Broadcast, так що кожне призначення буде отримувати повідомлення PoisonPill. Зауважте, що це зупинить всі призначення, навість якщо призначення не є дітьми маршрутизатора, тобто, коли призначення програмно надані маршрутизатору.
- import akka.actor.PoisonPill
- import akka.routing.Broadcast
- router ! Broadcast(PoisonPill)
З кодом, показаним вище, кожне призначення буде отримувати повідомлення PoisonPill. Кожне призначення буде продовжувати обробляти свої повідомлення, як звичано, з часом оброблячи PoisonPill. Це призведе до зупинки призначення. Після того, як всі призначення будуть зупинені, маршрутизатор сам по собі буде зупинений автоматично, тільки якщо це не динамічний маршрутизатор, тобто з використанням ресайзера.
Повідомлення Kill
Kill повідомлення є іншим типом повідомлень, що мають особливу обробку. Дивіться Вбивство актора щодо загальної інформації, як актори обробляють повідомлення Kill.
Коли повідомлення Kill надсилається до маршрутизатора, той обробляє повідомлення внутрішньо, та не посилає його своїм призначенням. Маршрутизатор буде викликати ActorKilledException та давати збій. Після цього він буде або відновлений, рестартований, або завершений, в залежності від дого, як налаштований його супервізор.
Призначення, що є дітями такого маршрутизатора, також будуть призупинені, та на них буде впливати директива супервізора, що стосується маршрутизатора. Призначення, що не є дитьми маршрутизатора, тобто ті, що були створені ззовні маршрутизатора, не зазнають вплива.
- import akka.actor.Kill
- router ! Kill
Як і з повідомленням PoisonPill, є різниця між вбивством маршрутизатора, що непрямо вбиває його дітей (які також є його призначеннями), та прямим вбивством призначень (деякі з яких можуть і не бути його дітьми). Щоб вбити призначення напряму, маршрутизатор повинен надіслати повідомлення Kill, огорнуте в повідомлення Broadcast.
- import akka.actor.Kill
- import akka.routing.Broadcast
- router ! Broadcast(Kill)
Керування повідомленнями
- Надсилання
akka.routing.GetRoutees до актора-маршрутизатора буде отримувати назад поточні задіяні призначення, в вигляді повідомлення akka.routing.Routees. - Надсилання
akka.routing.AddRoutee актору-маршрутизатору буде додавати це призначення до колекції призначень. - Надсилання
akka.routing.RemoveRoutee до актора-маршрутизатора буде видаляти це призначення з колекції призначень. - Надсилання
akka.routing.AdjustPoolSize до актора-маршритазатора пула буде додавати або видаляти таке число призначень до його колекції призначень.
Ці керівні повідомлення можуть бути оброблені після інших повідомлень, так що коли ви надіслали AddRoutee , за яким безпосередньо слідує звичайне повідомлення, ви не гарантовані, що призначення були змінені при пересиланні звичайного повідомлення. Якщо вам треба знати, чи ваші зміни були застосовані, надішліть AddRoutee, за яким GetRoutees, то коли ви отримаєте відповідь Routees, ви знатимите, що обробка зміни була застосована.
Динамічно зростаючий пул
Більшість пулів можуть бути використані з фіксованим числом призначень, або зі стратегією ресайзинга, для динамічного налаштування числа призначень.
Є два типи ресайзерів: Resizer по замовчанню, та OptimalSizeExploringResizer.
Resizer по замовчанню
Ресайзер по замовчанню змінює ємність пула догори та вниз на основі тиску, що обчислюється як відсоток зайнятих призначень пула. Він збільшує пул, якщо тиск більше, ніж певний поріг, та відкатується назад, якщо тиск нижче, ніж нижній поріг. Обоє пороги налаштовуються.
Пул з ресайзером по замовчанню, визначений в конфігурації:
- akka.actor.deployment {
- /parent/router29 {
- router = round-robin-pool
- resizer {
- lower-bound = 2
- upper-bound = 15
- messages-per-resize = 100
- }
- }
- }
- val router29: ActorRef =
- context.actorOf(FromConfig.props(Props[Worker]), "router29")
Декілька більше опцій конфігурації are available and described in akka.actor.deployment.default.resizersection of the reference Configuration.
Пул з ресайзером, що визначений в коді:
- val resizer = DefaultResizer(lowerBound = 2, upperBound = 15)
- val router30: ActorRef =
- context.actorOf(
- RoundRobinPool(5, Some(resizer)).props(Props[Worker]),
- "router30")
Також слід зазначити, що якщо ми визначаємо ``router`` в файлі конфігурації, тоді це значення буде використане замість любих програмно встановлених параметрів.
Ресайзер з пошуком оптимального розміру
OptimalSizeExploringResizer змінює розмір пула до оптимального, що провадить найбільшу пропускну здібність повідомлень.
Цей ресайзер краще робить, коли ви очікуєте, що функція розміру файла до продуктивності буде опуклою. Наприклад, коли ви маєте завдання, що прив'язані до CPU, оптимальний розмір прив'язаний до числа ядер CPU. Коли ваше завдання прив'язане до IO, оптимальний розмір прив'язаний до оптимального числа одночасних з'єднань до цього пристрою IO - тобто, кластер з еластичним пошуком з 4 вузлів може обробляти 4-8 одночасних запитів з оптимальною швидкістю.
Це досягається відстеженням пропускної здібності повідомлень для кожного розміру пула, та периодичне виконання наступних трьох операцій зміни розміру (по одній за раз):
- Зменшення, якщо не видно, що всі призначення будь-коли повністю навантажені деякій період часу.
- Дослідження випадкових сусідніх розмірів пула, та спроба зібрати метрики пропускної здібності.
- Optimize to a nearby pool size with a better (than any other nearby sizes) throughput metrics.
When the pool is fully-utilized (i.e. all routees are busy), it randomly choose between exploring and optimizing. When the pool has not been fully-utilized for a period of time, it will downsize the pool to the last seen max utilization multiplied by a configurable ratio.
By constantly exploring and optimizing, the resizer will eventually walk to the optimal size and remain nearby. When the optimal size changes it will start walking towards the new one.
It keeps a performance log so it's stateful as well as having a larger memory footprint than the default Resizer. The memory usage is O(n) where n is the number of sizes you allow, i.e. upperBound - lowerBound.
Pool with OptimalSizeExploringResizer defined in configuration:
- akka.actor.deployment {
- /parent/router31 {
- router = round-robin-pool
- optimal-size-exploring-resizer {
- enabled = on
- action-interval = 5s
- downsize-after-underutilized-for = 72h
- }
- }
- }
- val router31: ActorRef =
- context.actorOf(FromConfig.props(Props[Worker]), "router31")
Several more configuration options are available and described in akka.actor.deployment.default.optimal-size-exploring-resizer section of the reference Configuration.
Note
Resizing is triggered by sending messages to the actor pool, but it is not completed synchronously; instead a message is sent to the “head” RouterActor to perform the size change. Thus you cannot rely on resizing to instantaneously create new workers when all others are busy, because the message just sent will be queued to the mailbox of a busy actor. To remedy this, configure the pool to use a balancing dispatcher, see Configuring Dispatchers for more information.
How Routing is Designed within Akka
On the surface routers look like normal actors, but they are actually implemented differently. Routers are designed to be extremely efficient at receiving messages and passing them quickly on to routees.
A normal actor can be used for routing messages, but an actor's single-threaded processing can become a bottleneck. Routers can achieve much higher throughput with an optimization to the usual message-processing pipeline that allows concurrent routing. This is achieved by embedding routers' routing logic directly in their ActorRef rather than in the router actor. Messages sent to a router's ActorRef can be immediately routed to the routee, bypassing the single-threaded router actor entirely.
The cost to this is, of course, that the internals of routing code are more complicated than if routers were implemented with normal actors. Fortunately all of this complexity is invisible to consumers of the routing API. However, it is something to be aware of when implementing your own routers.
Custom Router
You can create your own router should you not find any of the ones provided by Akka sufficient for your needs. In order to roll your own router you have to fulfill certain criteria which are explained in this section.
Before creating your own router you should consider whether a normal actor with router-like behavior might do the job just as well as a full-blown router. As explained above, the primary benefit of routers over normal actors is their higher performance. But they are somewhat more complicated to write than normal actors. Therefore if lower maximum throughput is acceptable in your application you may wish to stick with traditional actors. This section, however, assumes that you wish to get maximum performance and so demonstrates how you can create your own router.
The router created in this example is replicating each message to a few destinations.
Start with the routing logic:
- import scala.collection.immutable
- import java.util.concurrent.ThreadLocalRandom
- import akka.routing.RoundRobinRoutingLogic
- import akka.routing.RoutingLogic
- import akka.routing.Routee
- import akka.routing.SeveralRoutees
-
- class RedundancyRoutingLogic(nbrCopies: Int) extends RoutingLogic {
- val roundRobin = RoundRobinRoutingLogic()
- def select(message: Any, routees: immutable.IndexedSeq[Routee]): Routee = {
- val targets = (1 to nbrCopies).map(_ => roundRobin.select(message, routees))
- SeveralRoutees(targets)
- }
- }
select will be called for each message and in this example pick a few destinations by round-robin, by reusing the existing RoundRobinRoutingLogic and wrap the result in a SeveralRoutees instance. SeveralRoutees will send the message to all of the supplied routes.
The implementation of the routing logic must be thread safe, since it might be used outside of actors.
A unit test of the routing logic:
- final case class TestRoutee(n: Int) extends Routee {
- override def send(message: Any, sender: ActorRef): Unit = ()
- }
-
- val logic = new RedundancyRoutingLogic(nbrCopies = 3)
-
- val routees = for (n <- 1 to 7) yield TestRoutee(n)
-
- val r1 = logic.select("msg", routees)
- r1.asInstanceOf[SeveralRoutees].routees should be(
- Vector(TestRoutee(1), TestRoutee(2), TestRoutee(3)))
-
- val r2 = logic.select("msg", routees)
- r2.asInstanceOf[SeveralRoutees].routees should be(
- Vector(TestRoutee(4), TestRoutee(5), TestRoutee(6)))
-
- val r3 = logic.select("msg", routees)
- r3.asInstanceOf[SeveralRoutees].routees should be(
- Vector(TestRoutee(7), TestRoutee(1), TestRoutee(2)))
You could stop here and use the RedundancyRoutingLogic with a akka.routing.Router as described in A Simple Router.
Let us continue and make this into a self contained, configurable, router actor.
Create a class that extends Pool, Group or CustomRouterConfig. That class is a factory for the routing logic and holds the configuration for the router. Here we make it a Group.
- import akka.dispatch.Dispatchers
- import akka.routing.Group
- import akka.routing.Router
- import akka.japi.Util.immutableSeq
- import com.typesafe.config.Config
-
- final case class RedundancyGroup(routeePaths: immutable.Iterable[String], nbrCopies: Int) extends Group {
-
- def this(config: Config) = this(
- routeePaths = immutableSeq(config.getStringList("routees.paths")),
- nbrCopies = config.getInt("nbr-copies"))
-
- override def paths(system: ActorSystem): immutable.Iterable[String] = routeePaths
-
- override def createRouter(system: ActorSystem): Router =
- new Router(new RedundancyRoutingLogic(nbrCopies))
-
- override val routerDispatcher: String = Dispatchers.DefaultDispatcherId
- }
This can be used exactly as the router actors provided by Akka.
- for (n <- 1 to 10) system.actorOf(Props[Storage], "s" + n)
-
- val paths = for (n <- 1 to 10) yield ("/user/s" + n)
- val redundancy1: ActorRef =
- system.actorOf(
- RedundancyGroup(paths, nbrCopies = 3).props(),
- name = "redundancy1")
- redundancy1 ! "important"
Note that we added a constructor in RedundancyGroup that takes a Config parameter. That makes it possible to define it in configuration.
- akka.actor.deployment {
- /redundancy2 {
- router = "docs.routing.RedundancyGroup"
- routees.paths = ["/user/s1", "/user/s2", "/user/s3"]
- nbr-copies = 5
- }
- }
Note the fully qualified class name in the router property. The router class must extendakka.routing.RouterConfig (Pool, Group or CustomRouterConfig) and have constructor with onecom.typesafe.config.Config parameter. The deployment section of the configuration is passed to the constructor.
- val redundancy2: ActorRef = system.actorOf(
- FromConfig.props(),
- name = "redundancy2")
- redundancy2 ! "very important"
Configuring Dispatchers
The dispatcher for created children of the pool will be taken from Props as described in Dispatchers.
To make it easy to define the dispatcher of the routees of the pool you can define the dispatcher inline in the deployment section of the config.
- akka.actor.deployment {
- /poolWithDispatcher {
- router = random-pool
- nr-of-instances = 5
- pool-dispatcher {
- fork-join-executor.parallelism-min = 5
- fork-join-executor.parallelism-max = 5
- }
- }
- }
That is the only thing you need to do enable a dedicated dispatcher for a pool.
Note
If you use a group of actors and route to their paths, then they will still use the same dispatcher that was configured for them in their Props, it is not possible to change an actors dispatcher after it has been created.
The “head” router cannot always run on the same dispatcher, because it does not process the same type of messages, hence this special actor does not use the dispatcher configured in Props, but takes the routerDispatcher from theRouterConfig instead, which defaults to the actor system’s default dispatcher. All standard routers allow setting this property in their constructor or factory method, custom routers have to implement the method in a suitable way.
- val router: ActorRef = system.actorOf(
- // “head” router actor will run on "router-dispatcher" dispatcher
- // Worker routees will run on "pool-dispatcher" dispatcher
- RandomPool(5, routerDispatcher = "router-dispatcher").props(Props[Worker]),
- name = "poolWithDispatcher")
Note
It is not allowed to configure the routerDispatcher to be aakka.dispatch.BalancingDispatcherConfigurator since the messages meant for the special router actor cannot be processed by any other actor.
Кінцеві автомати (FSM)
Огляд
FSM (Finite State Machine) доступний як мікс-ін для акторів Akka Actor, та найкраще описаний в принципах розробки Erlang
FSM може бути описаний як набір відношень в формі:
State(S) x Event(E) -> Actions (A), State(S')
Ці відношення інтерпретуються в наступному значенні:
Якщо ми є в стані S, та трапляється подія E, нам треба виконати дії A, та зробити перехід до стану S'.
Простий приклад
Щоб продемонструвати більшість з можливостей трейта FSM, розглянемо актора, який буде отримувати та ставити в чергу повідомлення, коли вони надходять в вибуховому режимі, та надсилає їх після спаду напливу, або коли надійде запит скинути повідомлення.
Перше, вважатимемо, що все нижче використовує такі твердження імпорта:
- import akka.actor.{ ActorRef, FSM }
- import scala.concurrent.duration._
Контракт нашого актора-накопичувача “Buncher” поялгає в тому, що він приймає або продукує наступні повідомлення:
- // отримані повідомлення
- final case class SetTarget(ref: ActorRef)
- final case class Queue(obj: Any)
- case object Flush
-
- // надіслані повідомлення
- final case class Batch(obj: immutable.Seq[Any])
SetTarget потрібний для початку роботи, що вказує призначення, куди треба передавати Batches; Queue буде додавати до внутрішньої черги, тоді як Flush буде визначати завершення навали.
- // стани
- sealed trait State
- case object Idle extends State
- case object Active extends State
-
- sealed trait Data
- case object Uninitialized extends Data
- final case class Todo(target: ActorRef, queue: immutable.Seq[Any]) extends Data
Актор може знаходитись в двох станах: повідомлень немає в черзі (тобто Idle), або деякі є в черзі (тобто Active). Він буде знаходитись в активному стані доти, докі повідомлення будуть продовжувати надходити, та не буде запита на скидання. Початковий стан даних актора складається з посилання на цільового актора, куди надсилати пакунки, та справжня черга повідомлень.
Тепер давайте поглянемо на скелет для нашого актора FSM:
- class Buncher extends FSM[State, Data] {
-
- startWith(Idle, Uninitialized)
-
- when(Idle) {
- case Event(SetTarget(ref), Uninitialized) =>
- stay using Todo(ref, Vector.empty)
- }
-
- // transition опущено ...
-
- when(Active, stateTimeout = 1 second) {
- case Event(Flush | StateTimeout, t: Todo) =>
- goto(Idle) using t.copy(queue = Vector.empty)
- }
-
- // unhandled опущено ...
-
- initialize()
- }
Базова стратегія полягає в декларуванні актора, міксуванні трейтаFSM, та вказанні можливих станів та значень даних як параметрів типа. В тілі актора використовується DSL для декларування машини станів:
startWith визначає початковий стан та початкові дані- потім іде одна декларація
when(<state>) { ... } на стан, що обробляється (може потенційно бути декількома, передані часткові функції PartialFunction будуть конкатеновані за допомогою orElse) - нарешті, все запускається з використанням
initialize, що виконує перехід до початкового стану та встановлює таймери (якщо потрібно).
В цьому випадку ми починаємо в стані Idle та Uninitialized, де обробляються тільки повідомлення SetTarget();stay готує завершення обробки цього повідомлення, не покидаючи поточного стану, тоді як подифікатор using змушує FSM змінити внутрішній стан (що є Uninitialized в цій точці) на новий об'єкт Todo(), що містить посилання на цільового актора. Стан Active має задекларований таймаут стану, що означає, що якщо протягом однієї секунди не надійде жодного повідомленя, буде згенероване повідомлення FSM.StateTimeout. В цьому випадку це має той же ефект, що отримання команди Flush, а саме до переходу назад до стану Idle, та скидання внутрішньої черги в пустий вектор. Але як повідомлення стають в чергу? Оскільки це має робити однаково в обох станах, ми використовуємо факт, що кожна подія, що не оброблена блоком when() буде передана до блока whenUnhandled():
- whenUnhandled {
- // загальний стан для обох станів
- case Event(Queue(obj), t @ Todo(_, v)) =>
- goto(Active) using t.copy(queue = v :+ obj)
-
- case Event(e, s) =>
- log.warning("отримане необроблений запит {} в стані {}/{}", e, stateName, s)
- stay
- }
Перший оброблений тут випадок додає запити Queue() до внутрішньої черги, та переходить до стану Active (це також очевидно залишає в стані Active, якщо ви вже там), але тільки якщо дані FSM не Uninitialized, коли отримується подіяQueue(). Інакше — та в усіх інших необроблених випадках — жругий випадок тільки журналює попередження, та не змінює внутрішній стан.
Одна недостаюча тут річ, це де насправді Batches надсилається до цілі, для якої ми задіяли механізм onTransition: ви можете задекларувати декілька таких блоків, та всі вони будуть намагатись спробувати щодо співпадаючої поведінки в випадку зміни стану (тобто, коли стан насправді змінюється).
- onTransition {
- case Active -> Idle =>
- stateData match {
- case Todo(ref, queue) => ref ! Batch(queue)
- case _ => // нічого не робити
- }
- }
Зворотній виклик переходу є частковою функцією, що сприймає в якості входу пару станів — поточний та наступний стан. Трейт FSM включає зручний екстрактор для цього в формі оператора стрілки, що природно нагадує вам напрямок зміни стану, що порівнюється. Під час зміни стану старі дані стану доступні через stateData, як показано, та нові дані стану будуть доступні як nextStateData.
Зауваження
Перехід на той самий стан може бути реалізований (коли ви зараз в стані S) з використанням goto(S) або stay(). Різниця між ними полягає в тому, що goto(S) буде надсилати подію S->S, що буде оброблена onTransition, тоді як stay() не буде.
Щоб перевірити, що це дійсно робить, досить просто написати тест з використанням Тестування системи акторів, що запакована з трейтами ScalaTest в AkkaSpec:
- import akka.actor.Props
- import scala.collection.immutable
-
- object FSMDocSpec {
- // повідомлення та типи даних
- }
-
- class FSMDocSpec extends MyFavoriteTestFrameWorkPlusAkkaTestKit {
- import FSMDocSpec._
-
- // fsm код пропущено ...
-
- "simple finite state machine" must {
-
- "demonstrate NullFunction" in {
- class A extends FSM[Int, Null] {
- val SomeState = 0
- when(SomeState)(FSM.NullFunction)
- }
- }
-
- "batch correctly" in {
- val buncher = system.actorOf(Props(classOf[Buncher], this))
- buncher ! SetTarget(testActor)
- buncher ! Queue(42)
- buncher ! Queue(43)
- expectMsg(Batch(immutable.Seq(42, 43)))
- buncher ! Queue(44)
- buncher ! Flush
- buncher ! Queue(45)
- expectMsg(Batch(immutable.Seq(44)))
- expectMsg(Batch(immutable.Seq(45)))
- }
-
- "not batch if uninitialized" in {
- val buncher = system.actorOf(Props(classOf[Buncher], this))
- buncher ! Queue(42)
- expectNoMsg
- }
- }
- }
Посилання
Трейт та об'єкт FSM
Трейт FSM прямо наслідує від Actor, коли ви розширяєте FSM, ви маєте знати, що насправді ви створюєте актора:
- class Buncher extends FSM[State, Data] {
-
- // тіло fsm ...
-
- initialize()
- }
Зауваження
Трейт FSM визначає метод receive, що обробляє внутрішні методи та передає все інше через логіку FSM (згідно поточного стану). Коли ви перевизначаєте метод receive, пам'ятайте, що обробка таймаутів стану залежить від справжньої передачі повідомлень через логіку FSM.
Трейт FSM приймає два параметри:
- супертип для всіх назв станів, зазвичай запечатаний трейт з кейс об'єктами, що розширюють його,
- тип даних стану, що відстежуються самим модулем
FSM.
Зауваження
Дані стану разом зі іменем стану описують внутрішній стан кінечного автомата; якщо ви пристаєте до цієї схеми, та не додаєте змінних полів до класу FSM, ви маєте перевагу робити всі зміни внутрішнього стану явними, в небагатьох гарно відомих місцях.
Визначення станів
Стан визначається одним або більше викликами метода when(<name>[, stateTimeout = <timeout>])(stateFunction).
Надане ім'я має бути об'єктом, що за типом сумісний з перишм параметром типу, переданому в трейт FSM. Цей об'єкт використовується як хеш ключ, так що ви маєте переконатись, що він достойно реалізує equals та hashCode; зокрема він має бути незмінним. Простішим випадком для ціх вимог є кейс об'єкти.
Якщо наданий параметр stateTimeout, тоді всі переходи в цей стан, включаючи залишення в ньому, отримають цей таймаут по замовчанню. Ініціація переходу з явно заданим таймаутом може перекрити це замовчання, дивіться Ініціація Переходів для додаткової інформацїі. Таймаут стану може бути змінений під час обробки за допомогою setStateTimeout(state, duration). Це дозволяє конфігурацію під час виконання, тобто через зовнішнє повідомленя.
Аргумент stateFunction є PartialFunction[Event, State], що зручно надається з використанням внутрішнього синтаксису часткової функції, як продемонстровано нижче:
- when(Idle) {
- case Event(SetTarget(ref), Uninitialized) =>
- stay using Todo(ref, Vector.empty)
- }
-
- when(Active, stateTimeout = 1 second) {
- case Event(Flush | StateTimeout, t: Todo) =>
- goto(Idle) using t.copy(queue = Vector.empty)
- }
The Event(msg: Any, data: D) case class is parameterized with the data type held by the FSM for convenient pattern matching.
Warning
It is required that you define handlers for each of the possible FSM states, otherwise there will be failures when trying to switch to undeclared states.
It is recommended practice to declare the states as objects extending a sealed trait and then verify that there is a whenclause for each of the states. If you want to leave the handling of a state “unhandled” (more below), it still needs to be declared like this:
- when(SomeState)(FSM.NullFunction)
Defining the Initial State
Each FSM needs a starting point, which is declared using
startWith(state, data[, timeout])
The optionally given timeout argument overrides any specification given for the desired initial state. If you want to cancel a default timeout, use None.
Unhandled Events
If a state doesn't handle a received event a warning is logged. If you want to do something else in this case you can specify that with whenUnhandled(stateFunction):
- whenUnhandled {
- case Event(x: X, data) =>
- log.info("Received unhandled event: " + x)
- stay
- case Event(msg, _) =>
- log.warning("Received unknown event: " + msg)
- goto(Error)
- }
Within this handler the state of the FSM may be queried using the stateName method.
ВАЖЛИВО: Цей обробник не накладається, тобто кожний виклик whenUnhandled заміщує попередньо встановлений обробник.
Ініціація зміни стану
Результатом кожного stateFunction має бути визначення наступного стану, або зупинка FSM, що описане в Зупинка зсередини. Визначення стану може бути або поточний стан, що задається директивою stay, або інший стан, що дає goto(state). Отриманий об'єкт дозволяє подальшу кваліфікацію шляхом надання наступних модифікаторів:
forMax(duration)
Цей модифікатор встановлює таймаут наступного стану. Це означає, що стартує таймер, що по закінченню надсилає повідомлення StateTimeout до FSM. Цей таймер перериває роботу при кожному повідомленні, що надходить під час очікування; ви можете розраховувати на те, що повідомлення StateTimeout не надійде після отримання іншого повідомлення.
Цей модифікатор також використовується для перевизначення таймаута по замовчання, що вказаний на цільовому стані. Якщо ви бажаєте відмінити таймаут по замовчанню, використовуйте Duration.Inf.
using(data)
Цей модифікатор замінює старі дані стану на нові надані дані. Якщо ви слідуєте пораді вище, це єдине місце, де внутрішній стан даних взагалі модифікується.
replying(msg)
Цей модифікатор надсилає відповідь до наразі оброблюваного повідомлення, та більше ніяк не модифікує перехід стану.
Всі модифікатори можуть бути зціплені, щоб досягти гароного та стислого опису:
- when(SomeState) {
- case Event(msg, _) =>
- goto(Processing) using (newData) forMax (5 seconds) replying (WillDo)
- }
Дужки насправді не потрібні у всіх випадках, але вони візуально розділяють модифікатори та їх аргументи, і, таким чином, роблять код навіть більш приємним для читання сторонніми.
Зауваження
Будь ласка зауважте, що твердження return не може використовуватись в блоках when, або подібних; це обмеження Scala. Або перепишіть ваш код за допомогою if () ... else ..., або винесіть його в визначення метода.
Моніторинг зміни стану
Переходи відбуваються "між станами" концептуально, що означає після любих дій, що ви поклали в блок обробки події; це очевидне, оскільки інший стан визначений значенням, що повертається логікою обробки події. Вам не треба турбуватись щодо точного порядку, що встановлює змінну стану, тому що все в акторі FSM все одно виконується в одному потоці.
Внутрішній мониторинг
До цього місця FSM DSL був сконцентрований на станах та подіях. Подвійний погляд полягає в тому, щоб розглядати це як серію трансформацій. Це досягаєтсья методом
onTransition(handler)
що асоціює дії з переходами, замість станій та подій. Обробник є частковою функцією, що приймає пару станів на вході; результуючий стан не потрібен, бо неможливо трансформувати перехід в процессі.
- onTransition {
- case Idle -> Active => setTimer("timeout", Tick, 1 second, repeat = true)
- case Active -> _ => cancelTimer("timeout")
- case x -> Idle => log.info("entering Idle from " + x)
- }
Зручний екстрактор -> дозволяє докомпозицію пари станів з ясним візуальним нагадуванням напрямку переходу. Як звичайно в порівнянні шаблонів, може використовуватись підкреслення для нецікавих частей; альтернативно ви можете прікрипити необмежений стан до змінної, наприклад, для журналювання, як показане в останньому випадку.
Також можливо передати об'єкт-функцію, що приймає два стани, до onTransition, в випадку, коли ваша логіка переходу реалізована як метод:
- onTransition(handler _)
-
- def handler(from: StateType, to: StateType) {
- // handle it here ...
- }
Зареєстровані таким чином обробники накладаються, так що ви можете перемежати блоки onTransition з блоками when, що відповідає вашому дизайну. Однак слід зауважити, що всі блоки будуть задіяні для кожного переходу, не тільки перший співпавший блок. Це зроблено навмисне, так що ви можете покласти всю обробку переходу для окремого аспекта в одне місце, при цьому не турбуючись, що ранні декларації затінять наступні; при цьому дії виконуються в порядку декларування.
Зауваження
Цей тип внутрішнього моніторингу може бути використаний для структурування вашого FSM відповідно до переходів, так що, наприклад, відміна таймера під час виходу з певного стану не може бути забута, коли додаються нові цільові стани.
Зовнішній моніторинг
Зовнішні актори можуть бути зареєстровані для нотифікації про стан переходів, через надсилання повідомлення SubscribeTransitionCallBack(actorRef). Названому актору буде негайно надіслане повідомлення CurrentState(self, stateName), та він буде отримувати повідомлення Transition(actorRef, oldState, newState) при перемиканні зміни стану.
Будь ласка зауважте, що зміна стану включає дію по виконанню goto(S), тоді, коли ви вже в станіS. В цьому випадку моніторячий актор буде повідомлений за допомогою повідомлення Transition(ref,S,S). Це може бути корисним, якщо ваш FSM повинен реагувати на всі переходи (навіть без зміни стану). В випадку, коли ви скоріше не бажаєте генерації подій для переходів в той же стан, використовуйте stay() замість goto(S).
Зовнішні монітори можуть бути відреєстровані через надсилання UnsubscribeTransitionCallBack(actorRef) до FSM актора.
Зупинка слухача без відреєстрації не видалить слухача зі списку підписки; використовуйте UnsubscribeTransitionCallback перед зупинкою слухача.
Таймери
Окрім таймаутів стану FSM керує таймерами, що ідентифікуються за String іменами. Ви можете встановити таймер з використанням
setTimer(name, msg, interval, repeat)
де msg є об'єкт повідомлення, що буде надісланий після затримки interval. Якщо repeat є true, тоді таймер планується зі сталою частотою, що надається параметром interval. Любий існуючий таймер з тим самим ім'ям буде автоматично скасований перед додаванням нового таймера.
Таймери можуть бути скасовані з використанням
cancelTimer(name)
що гарантовано спрацьовує безпосередньо, що означає, що заплановані повідомлення не будуть оброблені після цього виклику, навіть якщо таймер вже спрацював та був поставлений в чергу. Статус любого таймера може бути опитаний за допомогою
isTimerActive(name)
Ці іменовані таймери доповнюють таймаути стану, оскільки вони не впливають на отримку інших повідомлень.
Завершення ззовні
FSM зупиняється через задання результуючого стану як
stop([reason[, data]])
Причиною може бути одна з: Normal (по замовчанню), Shutdown або Failure(reason), та може бути наданий другий аргумент, щоб змінити дані стану, що доступні на протязі обробки завершення.
Зауваження
Треба зауважити, що stop не перериває дії, та не зупиняє FSM безпосередньо. Дія stop має повертатись з обробника подій в той же спосіб, що і перехід стану (але зауважте, що твердження return не може бути використане в блоці when).
- when(Error) {
- case Event("stop", _) =>
- // робимо очистку ...
- stop()
- }
Ви можете використовувати onTermination(handler) для задання власного кода, що виконуєтся, коли FSM зупиняється. Обробник є частковою функцією, що приймає StopEvent(reason, stateName, stateData) в якості аргумента:
- onTermination {
- case StopEvent(FSM.Normal, state, data) => // ...
- case StopEvent(FSM.Shutdown, state, data) => // ...
- case StopEvent(FSM.Failure(cause), state, data) => // ...
- }
Як і для випадка whenUnhandled, цей обробник не накладається, так що кожний виклик onTermination замінює попередньо встановлений обробник.
Завершення ззовні Outside
Коли ActorRef, асоційований з FSM, зупиняється is stopped using the stop method, its postStop hook will be executed. The default implementation by the FSM trait is to execute the onTermination handler if that is prepared to handle aStopEvent(Shutdown, ...).
Warning
In case you override postStop and want to have your onTermination handler called, do not forget to callsuper.postStop.
Testing and Debugging Finite State Machines
During development and for trouble shooting FSMs need care just as any other actor. There are specialized tools available as described in Testing Finite State Machines and in the following.
Event Tracing
The setting akka.actor.debug.fsm in Configuration enables logging of an event trace by LoggingFSM instances:
- import akka.actor.LoggingFSM
- class MyFSM extends LoggingFSM[StateType, Data] {
- // body elided ...
- }
This FSM will log at DEBUG level:
- all processed events, including
StateTimeout and scheduled timer messages - every setting and cancellation of named timers
- all state transitions
Life cycle changes and special messages can be logged as described for Actors.
Rolling Event Log
The LoggingFSM trait adds one more feature to the FSM: a rolling event log which may be used during debugging (for tracing how the FSM entered a certain failure state) or for other creative uses:
- import akka.actor.LoggingFSM
- class MyFSM extends LoggingFSM[StateType, Data] {
- override def logDepth = 12
- onTermination {
- case StopEvent(FSM.Failure(_), state, data) =>
- val lastEvents = getLog.mkString("\n\t")
- log.warning("Failure in state " + state + " with data " + data + "\n" +
- "Events leading up to this point:\n\t" + lastEvents)
- }
- // ...
- }
The logDepth defaults to zero, which turns off the event log.
Warning
The log buffer is allocated during actor creation, which is why the configuration is done using a virtual method call. If you want to override with a val, make sure that its initialization happens before the initializer of LoggingFSMruns, and do not change the value returned by logDepth after the buffer has been allocated.
The contents of the event log are available using method getLog, which returns an IndexedSeq[LogEntry] where the oldest entry is at index zero.
Persistence
Akka persistence enables stateful actors to persist their internal state so that it can be recovered when an actor is started, restarted after a JVM crash or by a supervisor, or migrated in a cluster. The key concept behind Akka persistence is that only changes to an actor's internal state are persisted but never its current state directly (except for optional snapshots). These changes are only ever appended to storage, nothing is ever mutated, which allows for very high transaction rates and efficient replication. Stateful actors are recovered by replaying stored changes to these actors from which they can rebuild internal state. This can be either the full history of changes or starting from a snapshot which can dramatically reduce recovery times. Akka persistence also provides point-to-point communication with at-least-once message delivery semantics.
Akka persistence is inspired by and the official replacement of the eventsourced library. It follows the same concepts and architecture of eventsourced but significantly differs on API and implementation level. See also Migration Guide Eventsourced to Akka Persistence 2.3.x
Dependencies
Akka persistence is a separate jar file. Make sure that you have the following dependency in your project:
- "com.typesafe.akka" %% "akka-persistence" % "2.4.9"
The Akka persistence extension comes with few built-in persistence plugins, including in-memory heap based journal, local file-system based snapshot-store and LevelDB based journal.
LevelDB based plugins will require the following additional dependency declaration:
- "org.iq80.leveldb" % "leveldb" % "0.7"
- "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8"
Architecture
- PersistentActor: Is a persistent, stateful actor. It is able to persist events to a journal and can react to them in a thread-safe manner. It can be used to implement both command as well as event sourced actors. When a persistent actor is started or restarted, journaled messages are replayed to that actor so that it can recover internal state from these messages.
- PersistentView: A view is a persistent, stateful actor that receives journaled messages that have been written by another persistent actor. A view itself does not journal new messages, instead, it updates internal state only from a persistent actor's replicated message stream.
- AtLeastOnceDelivery: To send messages with at-least-once delivery semantics to destinations, also in case of sender and receiver JVM crashes.
- AsyncWriteJournal: A journal stores the sequence of messages sent to a persistent actor. An application can control which messages are journaled and which are received by the persistent actor without being journaled. Journal maintainshighestSequenceNr that is increased on each message. The storage backend of a journal is pluggable. The persistence extension comes with a "leveldb" journal plugin, which writes to the local filesystem. Replicated journals are available asCommunity plugins.
- Snapshot store: A snapshot store persists snapshots of a persistent actor's or a view's internal state. Snapshots are used for optimizing recovery times. The storage backend of a snapshot store is pluggable. The persistence extension comes with a "local" snapshot storage plugin, which writes to the local filesystem. Replicated snapshot stores are available as Community plugins.
Event sourcing
The basic idea behind Event Sourcing is quite simple. A persistent actor receives a (non-persistent) command which is first validated if it can be applied to the current state. Here validation can mean anything, from simple inspection of a command message's fields up to a conversation with several external services, for example. If validation succeeds, events are generated from the command, representing the effect of the command. These events are then persisted and, after successful persistence, used to change the actor's state. When the persistent actor needs to be recovered, only the persisted events are replayed of which we know that they can be successfully applied. In other words, events cannot fail when being replayed to a persistent actor, in contrast to commands. Event sourced actors may of course also process commands that do not change application state such as query commands for example.
Akka persistence supports event sourcing with the PersistentActor trait. An actor that extends this trait uses thepersist method to persist and handle events. The behavior of a PersistentActor is defined by implementingreceiveRecover and receiveCommand. This is demonstrated in the following example.
- import akka.actor._
- import akka.persistence._
-
- case class Cmd(data: String)
- case class Evt(data: String)
-
- case class ExampleState(events: List[String] = Nil) {
- def updated(evt: Evt): ExampleState = copy(evt.data :: events)
- def size: Int = events.length
- override def toString: String = events.reverse.toString
- }
-
- class ExamplePersistentActor extends PersistentActor {
- override def persistenceId = "sample-id-1"
-
- var state = ExampleState()
-
- def updateState(event: Evt): Unit =
- state = state.updated(event)
-
- def numEvents =
- state.size
-
- val receiveRecover: Receive = {
- case evt: Evt => updateState(evt)
- case SnapshotOffer(_, snapshot: ExampleState) => state = snapshot
- }
-
- val receiveCommand: Receive = {
- case Cmd(data) =>
- persist(Evt(s"${data}-${numEvents}"))(updateState)
- persist(Evt(s"${data}-${numEvents + 1}")) { event =>
- updateState(event)
- context.system.eventStream.publish(event)
- }
- case "snap" => saveSnapshot(state)
- case "print" => println(state)
- }
-
- }
The example defines two data types, Cmd and Evt to represent commands and events, respectively. The state of the ExamplePersistentActor is a list of persisted event data contained in ExampleState.
The persistent actor's receiveRecover method defines how state is updated during recovery by handling Evtand SnapshotOffer messages. The persistent actor's receiveCommand method is a command handler. In this example, a command is handled by generating two events which are then persisted and handled. Events are persisted by calling persist with an event (or a sequence of events) as first argument and an event handler as second argument.
The persist method persists events asynchronously and the event handler is executed for successfully persisted events. Successfully persisted events are internally sent back to the persistent actor as individual messages that trigger event handler executions. An event handler may close over persistent actor state and mutate it. The sender of a persisted event is the sender of the corresponding command. This allows event handlers to reply to the sender of a command (not shown).
The main responsibility of an event handler is changing persistent actor state using event data and notifying others about successful state changes by publishing events.
When persisting events with persist it is guaranteed that the persistent actor will not receive further commands between the persist call and the execution(s) of the associated event handler. This also holds for multiple persistcalls in context of a single command. Incoming messages are stashed until the persist is completed.
If persistence of an event fails, onPersistFailure will be invoked (logging the error by default), and the actor will unconditionally be stopped. If persistence of an event is rejected before it is stored, e.g. due to serialization error,onPersistRejected will be invoked (logging a warning by default) and the actor continues with the next message.
The easiest way to run this example yourself is to download Lightbend Activator and open the tutorial named Akka Persistence Samples with Scala. It contains instructions on how to run the PersistentActorExample.
Note
It's also possible to switch between different command handlers during normal processing and recovery withcontext.become() and context.unbecome(). To get the actor into the same state after recovery you need to take special care to perform the same state transitions with become and unbecome in the receiveRecovermethod as you would have done in the command handler. Note that when using become from receiveRecoverit will still only use the receiveRecover behavior when replaying the events. When replay is completed it will use the new behavior.
Identifiers
A persistent actor must have an identifier that doesn't change across different actor incarnations. The identifier must be defined with the persistenceId method.
- override def persistenceId = "my-stable-persistence-id"
Recovery
By default, a persistent actor is automatically recovered on start and on restart by replaying journaled messages. New messages sent to a persistent actor during recovery do not interfere with replayed messages. They are cached and received by a persistent actor after recovery phase completes.
Note
Accessing the sender() for replayed messages will always result in a deadLetters reference, as the original sender is presumed to be long gone. If you indeed have to notify an actor during recovery in the future, store itsActorPath explicitly in your persisted events.
Recovery customization
Applications may also customise how recovery is performed by returning a customised Recovery object in therecovery method of a PersistentActor, for example setting an upper bound to the replay which allows the actor to be replayed to a certain point "in the past" instead to its most up to date state:
- override def recovery = Recovery(toSequenceNr = 457L)
Recovery can be disabled by returning Recovery.none() in the recovery method of a PersistentActor:
- override def recovery = Recovery.none
Recovery status
A persistent actor can query its own recovery status via the methods
- def recoveryRunning: Boolean
- def recoveryFinished: Boolean
Sometimes there is a need for performing additional initialization when the recovery has completed before processing any other message sent to the persistent actor. The persistent actor will receive a special RecoveryCompletedmessage right after recovery and before any other received messages.
- override def receiveRecover: Receive = {
- case RecoveryCompleted =>
- // perform init after recovery, before any other messages
- //...
- case evt => //...
- }
-
- override def receiveCommand: Receive = {
- case msg => //...
- }
If there is a problem with recovering the state of the actor from the journal, onRecoveryFailure is called (logging the error by default) and the actor will be stopped.
Internal stash
The persistent actor has a private stash for internally caching incoming messages during recovery or thepersist\persistAll method persisting events. You can still use/inherit from the Stash interface. The internal stash cooperates with the normal stash by hooking into unstashAll method and making sure messages are unstashed properly to the internal stash to maintain ordering guarantees.
You should be careful to not send more messages to a persistent actor than it can keep up with, otherwise the number of stashed messages will grow without bounds. It can be wise to protect against OutOfMemoryError by defining a maximum stash capacity in the mailbox configuration:
- akka.actor.default-mailbox.stash-capacity=10000
Note that the stash capacity is per actor. If you have many persistent actors, e.g. when using cluster sharding, you may need to define a small stash capacity to ensure that the total number of stashed messages in the system don't consume too much memory. Additionally, The persistent actor defines three strategies to handle failure when the internal stash capacity is exceeded. The default overflow strategy is the ThrowOverflowExceptionStrategy, which discards the current received message and throws a StashOverflowException, causing actor restart if default supervision strategy is used. you can override the internalStashOverflowStrategy method to returnDiscardToDeadLetterStrategy or ReplyToStrategy for any "individual" persistent actor, or define the "default" for all persistent actors by providing FQCN, which must be a subclass of StashOverflowStrategyConfigurator, in the persistence configuration:
- akka.persistence.internal-stash-overflow-strategy=
- "akka.persistence.ThrowExceptionConfigurator"
The DiscardToDeadLetterStrategy strategy also has a pre-packaged companion configuratorakka.persistence.DiscardConfigurator.
You can also query default strategy via the Akka persistence extension singleton:
- Persistence(context.system).defaultInternalStashOverflowStrategy
Note
The bounded mailbox should be avoided in the persistent actor, by which the messages come from storage backends may be discarded. You can use bounded stash instead of it.
Relaxed local consistency requirements and high throughput use-cases
If faced with relaxed local consistency requirements and high throughput demands sometimes PersistentActor and its persist may not be enough in terms of consuming incoming Commands at a high rate, because it has to wait until all Events related to a given Command are processed in order to start processing the next Command. While this abstraction is very useful for most cases, sometimes you may be faced with relaxed requirements about consistency – for example you may want to process commands as fast as you can, assuming that the Event will eventually be persisted and handled properly in the background, retroactively reacting to persistence failures if needed.
The persistAsync method provides a tool for implementing high-throughput persistent actors. It will not stash incoming Commands while the Journal is still working on persisting and/or user code is executing event callbacks.
In the below example, the event callbacks may be called "at any time", even after the next Command has been processed. The ordering between events is still guaranteed ("evt-b-1" will be sent after "evt-a-2", which will be sent after "evt-a-1" etc.).
- class MyPersistentActor extends PersistentActor {
-
- override def persistenceId = "my-stable-persistence-id"
-
- override def receiveRecover: Receive = {
- case _ => // handle recovery here
- }
-
- override def receiveCommand: Receive = {
- case c: String => {
- sender() ! c
- persistAsync(s"evt-$c-1") { e => sender() ! e }
- persistAsync(s"evt-$c-2") { e => sender() ! e }
- }
- }
- }
-
- // usage
- persistentActor ! "a"
- persistentActor ! "b"
-
- // possible order of received messages:
- // a
- // b
- // evt-a-1
- // evt-a-2
- // evt-b-1
- // evt-b-2
Note
In order to implement the pattern known as "command sourcing" simply call persistAsync(cmd)(...) right away on all incoming messages and handle them in the callback.
Warning
The callback will not be invoked if the actor is restarted (or stopped) in between the call to persistAsync and the journal has confirmed the write.
Deferring actions until preceding persist handlers have executed
Sometimes when working with persistAsync you may find that it would be nice to define some actions in terms of ''happens-after the previous persistAsync handlers have been invoked''. PersistentActor provides an utility method called deferAsync, which works similarly to persistAsync yet does not persist the passed in event. It is recommended to use it for read operations, and actions which do not have corresponding events in your domain model.
Using this method is very similar to the persist family of methods, yet it does not persist the passed in event. It will be kept in memory and used when invoking the handler.
- class MyPersistentActor extends PersistentActor {
-
- override def persistenceId = "my-stable-persistence-id"
-
- override def receiveRecover: Receive = {
- case _ => // handle recovery here
- }
-
- override def receiveCommand: Receive = {
- case c: String => {
- sender() ! c
- persistAsync(s"evt-$c-1") { e => sender() ! e }
- persistAsync(s"evt-$c-2") { e => sender() ! e }
- deferAsync(s"evt-$c-3") { e => sender() ! e }
- }
- }
- }
Notice that the sender() is safe to access in the handler callback, and will be pointing to the original sender of the command for which this deferAsync handler was called.
The calling side will get the responses in this (guaranteed) order:
- persistentActor ! "a"
- persistentActor ! "b"
-
- // order of received messages:
- // a
- // b
- // evt-a-1
- // evt-a-2
- // evt-a-3
- // evt-b-1
- // evt-b-2
- // evt-b-3
Warning
The callback will not be invoked if the actor is restarted (or stopped) in between the call to deferAsync and the journal has processed and confirmed all preceding writes.
Nested persist calls
It is possible to call persist and persistAsync inside their respective callback blocks and they will properly retain both the thread safety (including the right value of sender()) as well as stashing guarantees.
In general it is encouraged to create command handlers which do not need to resort to nested event persisting, however there are situations where it may be useful. It is important to understand the ordering of callback execution in those situations, as well as their implication on the stashing behaviour (that persist() enforces). In the following example two persist calls are issued, and each of them issues another persist inside its callback:
- override def receiveCommand: Receive = {
- case c: String =>
- sender() ! c
-
- persist(s"$c-1-outer") { outer1 =>
- sender() ! outer1
- persist(s"$c-1-inner") { inner1 =>
- sender() ! inner1
- }
- }
-
- persist(s"$c-2-outer") { outer2 =>
- sender() ! outer2
- persist(s"$c-2-inner") { inner2 =>
- sender() ! inner2
- }
- }
- }
When sending two commands to this PersistentActor, the persist handlers will be executed in the following order:
- persistentActor ! "a"
- persistentActor ! "b"
-
- // order of received messages:
- // a
- // a-outer-1
- // a-outer-2
- // a-inner-1
- // a-inner-2
- // and only then process "b"
- // b
- // b-outer-1
- // b-outer-2
- // b-inner-1
- // b-inner-2
First the "outer layer" of persist calls is issued and their callbacks are applied. After these have successfully completed, the inner callbacks will be invoked (once the events they are persisting have been confirmed to be persisted by the journal). Only after all these handlers have been successfully invoked will the next command be delivered to the persistent Actor. In other words, the stashing of incoming commands that is guaranteed by initially calling persist()on the outer layer is extended until all nested persist callbacks have been handled.
It is also possible to nest persistAsync calls, using the same pattern:
- override def receiveCommand: Receive = {
- case c: String =>
- sender() ! c
- persistAsync(c + "-outer-1") { outer =>
- sender() ! outer
- persistAsync(c + "-inner-1") { inner => sender() ! inner }
- }
- persistAsync(c + "-outer-2") { outer =>
- sender() ! outer
- persistAsync(c + "-inner-2") { inner => sender() ! inner }
- }
- }
In this case no stashing is happening, yet events are still persisted and callbacks are executed in the expected order:
- persistentActor ! "a"
- persistentActor ! "b"
-
- // order of received messages:
- // a
- // b
- // a-outer-1
- // a-outer-2
- // b-outer-1
- // b-outer-2
- // a-inner-1
- // a-inner-2
- // b-inner-1
- // b-inner-2
-
- // which can be seen as the following causal relationship:
- // a -> a-outer-1 -> a-outer-2 -> a-inner-1 -> a-inner-2
- // b -> b-outer-1 -> b-outer-2 -> b-inner-1 -> b-inner-2
While it is possible to nest mixed persist and persistAsync with keeping their respective semantics it is not a recommended practice, as it may lead to overly complex nesting.
Failures
If persistence of an event fails, onPersistFailure will be invoked (logging the error by default), and the actor will unconditionally be stopped.
The reason that it cannot resume when persist fails is that it is unknown if the event was actually persisted or not, and therefore it is in an inconsistent state. Restarting on persistent failures will most likely fail anyway since the journal is probably unavailable. It is better to stop the actor and after a back-off timeout start it again. Theakka.pattern.BackoffSupervisor actor is provided to support such restarts.
- val childProps = Props[MyPersistentActor]
- val props = BackoffSupervisor.props(
- Backoff.onStop(
- childProps,
- childName = "myActor",
- minBackoff = 3.seconds,
- maxBackoff = 30.seconds,
- randomFactor = 0.2))
- context.actorOf(props, name = "mySupervisor")
If persistence of an event is rejected before it is stored, e.g. due to serialization error, onPersistRejected will be invoked (logging a warning by default), and the actor continues with next message.
If there is a problem with recovering the state of the actor from the journal when the actor is started,onRecoveryFailure is called (logging the error by default), and the actor will be stopped.
Atomic writes
Each event is of course stored atomically, but it is also possible to store several events atomically by using thepersistAll or persistAllAsync method. That means that all events passed to that method are stored or none of them are stored if there is an error.
The recovery of a persistent actor will therefore never be done partially with only a subset of events persisted bypersistAll.
Some journals may not support atomic writes of several events and they will then reject the persistAll command, i.e. onPersistRejected is called with an exception (typically UnsupportedOperationException).
Batch writes
In order to optimize throughput when using persistAsync, a persistent actor internally batches events to be stored under high load before writing them to the journal (as a single batch). The batch size is dynamically determined by how many events are emitted during the time of a journal round-trip: after sending a batch to the journal no further batch can be sent before confirmation has been received that the previous batch has been written. Batch writes are never timer-based which keeps latencies at a minimum.
Message deletion
It is possible to delete all messages (journaled by a single persistent actor) up to a specified sequence number; Persistent actors may call the deleteMessages method to this end.
Deleting messages in event sourcing based applications is typically either not used at all, or used in conjunction withsnapshotting, i.e. after a snapshot has been successfully stored, a deleteMessages(toSequenceNr) up until the sequence number of the data held by that snapshot can be issued to safely delete the previous events while still having access to the accumulated state during replays - by loading the snapshot.
The result of the deleteMessages request is signaled to the persistent actor with a DeleteMessagesSuccessmessage if the delete was successful or a DeleteMessagesFailure message if it failed.
Message deletion doesn't affect the highest sequence number of the journal, even if all messages were deleted from it after deleteMessages invocation.
Persistence status handling
Persisting, deleting, and replaying messages can either succeed or fail.
| Method | Success | Failure / Rejection | After failure handler invoked |
persist / persistAsync | persist handler invoked | onPersistFailure | Actor is stopped. |
onPersistRejected | No automatic actions. |
recovery | RecoveryCompleted | onRecoveryFailure | Actor is stopped. |
deleteMessages | DeleteMessagesSuccess | DeleteMessagesFailure | No automatic actions. |
The most important operations (persist and recovery) have failure handlers modelled as explicit callbacks which the user can override in the PersistentActor. The default implementations of these handlers emit a log message (error for persist/recovery failures, and warning for others), logging the failure cause and information about which message caused the failure.
For critical failures, such as recovery or persisting events failing, the persistent actor will be stopped after the failure handler is invoked. This is because if the underlying journal implementation is signalling persistence failures it is most likely either failing completely or overloaded and restarting right-away and trying to persist the event again will most likely not help the journal recover – as it would likely cause a Thundering herd problem, as many persistent actors would restart and try to persist their events again. Instead, using a BackoffSupervisor (as described in Failures) which implements an exponential-backoff strategy which allows for more breathing room for the journal to recover between restarts of the persistent actor.
Note
Journal implementations may choose to implement a retry mechanism, e.g. such that only after a write fails N number of times a persistence failure is signalled back to the user. In other words, once a journal returns a failure, it is considered fatal by Akka Persistence, and the persistent actor which caused the failure will be stopped.
Check the documentation of the journal implementation you are using for details if/how it is using this technique.
Safely shutting down persistent actors
Special care should be given when shutting down persistent actors from the outside. With normal Actors it is often acceptable to use the special PoisonPill message to signal to an Actor that it should stop itself once it receives this message – in fact this message is handled automatically by Akka, leaving the target actor no way to refuse stopping itself when given a poison pill.
This can be dangerous when used with PersistentActor due to the fact that incoming commands are stashed while the persistent actor is awaiting confirmation from the Journal that events have been written when persist() was used. Since the incoming commands will be drained from the Actor's mailbox and put into its internal stash while awaiting the confirmation (thus, before calling the persist handlers) the Actor may receive and (auto)handle the PoisonPill before it processes the other messages which have been put into its stash, causing a pre-mature shutdown of the Actor.
Warning
Consider using explicit shut-down messages instead of PoisonPill when working with persistent actors.
The example below highlights how messages arrive in the Actor's mailbox and how they interact with its internal stashing mechanism when persist() is used. Notice the early stop behaviour that occurs when PoisonPill is used:
- /** Explicit shutdown message */
- case object Shutdown
-
- class SafePersistentActor extends PersistentActor {
- override def persistenceId = "safe-actor"
-
- override def receiveCommand: Receive = {
- case c: String =>
- println(c)
- persist(s"handle-$c") { println(_) }
- case Shutdown =>
- context.stop(self)
- }
-
- override def receiveRecover: Receive = {
- case _ => // handle recovery here
- }
- }
- // UN-SAFE, due to PersistentActor's command stashing:
- persistentActor ! "a"
- persistentActor ! "b"
- persistentActor ! PoisonPill
- // order of received messages:
- // a
- // # b arrives at mailbox, stashing; internal-stash = [b]
- // PoisonPill is an AutoReceivedMessage, is handled automatically
- // !! stop !!
- // Actor is stopped without handling `b` nor the `a` handler!
- // SAFE:
- persistentActor ! "a"
- persistentActor ! "b"
- persistentActor ! Shutdown
- // order of received messages:
- // a
- // # b arrives at mailbox, stashing; internal-stash = [b]
- // # Shutdown arrives at mailbox, stashing; internal-stash = [b, Shutdown]
- // handle-a
- // # unstashing; internal-stash = [Shutdown]
- // b
- // handle-b
- // # unstashing; internal-stash = []
- // Shutdown
- // -- stop --
Persistent Views
Warning
PersistentView is deprecated. Use Persistence Query instead. The corresponding query type isEventsByPersistenceId. There are several alternatives for connecting the Source to an actor corresponding to a previous PersistentView actor:
- Sink.actorRef is simple, but has the disadvantage that there is no back-pressure signal from the destination actor, i.e. if the actor is not consuming the messages fast enough the mailbox of the actor will grow
- mapAsync combined with Ask: Send-And-Receive-Future is almost as simple with the advantage of back-pressure being propagated all the way
- ActorSubscriber in case you need more fine grained control
The consuming actor may be a plain Actor or a PersistentActor if it needs to store its own state (e.g. fromSequenceNr offset).
Persistent views can be implemented by extending the PersistentView trait and implementing the receive and the persistenceId methods.
- class MyView extends PersistentView {
- override def persistenceId: String = "some-persistence-id"
- override def viewId: String = "some-persistence-id-view"
-
- def receive: Receive = {
- case payload if isPersistent =>
- // handle message from journal...
- case payload =>
- // handle message from user-land...
- }
- }
The persistenceId identifies the persistent actor from which the view receives journaled messages. It is not necessary that the referenced persistent actor is actually running. Views read messages from a persistent actor's journal directly. When a persistent actor is started later and begins to write new messages, by default the corresponding view is updated automatically.
It is possible to determine if a message was sent from the Journal or from another actor in user-land by calling theisPersistent method. Having that said, very often you don't need this information at all and can simply apply the same logic to both cases (skip the if isPersistent check).
Updates
The default update interval of all views of an actor system is configurable:
- akka.persistence.view.auto-update-interval = 5s
PersistentView implementation classes may also override the autoUpdateInterval method to return a custom update interval for a specific view class or view instance. Applications may also trigger additional updates at any time by sending a view an Update message.
- val view = system.actorOf(Props[MyView])
- view ! Update(await = true)
If the await parameter is set to true, messages that follow the Update request are processed when the incremental message replay, triggered by that update request, completed. If set to false (default), messages following the update request may interleave with the replayed message stream. Automated updates always run with await =false.
Automated updates of all persistent views of an actor system can be turned off by configuration:
- akka.persistence.view.auto-update = off
Implementation classes may override the configured default value by overriding the autoUpdate method. To limit the number of replayed messages per update request, applications can configure a customakka.persistence.view.auto-update-replay-max value or override the autoUpdateReplayMax method. The number of replayed messages for manual updates can be limited with the replayMax parameter of the Updatemessage.
Recovery
Initial recovery of persistent views works the very same way as for persistent actors (i.e. by sending a Recovermessage to self). The maximum number of replayed messages during initial recovery is determined byautoUpdateReplayMax. Further possibilities to customize initial recovery are explained in section Recovery.
Identifiers
A persistent view must have an identifier that doesn't change across different actor incarnations. The identifier must be defined with the viewId method.
The viewId must differ from the referenced persistenceId, unless Snapshots of a view and its persistent actor should be shared (which is what applications usually do not want).
Snapshots
Snapshots can dramatically reduce recovery times of persistent actors and views. The following discusses snapshots in context of persistent actors but this is also applicable to persistent views.
Persistent actors can save snapshots of internal state by calling the saveSnapshot method. If saving of a snapshot succeeds, the persistent actor receives a SaveSnapshotSuccess message, otherwise a SaveSnapshotFailuremessage
- var state: Any = _
-
- override def receiveCommand: Receive = {
- case "snap" => saveSnapshot(state)
- case SaveSnapshotSuccess(metadata) => // ...
- case SaveSnapshotFailure(metadata, reason) => // ...
- }
where metadata is of type SnapshotMetadata:
- final case class SnapshotMetadata(persistenceId: String, sequenceNr: Long, timestamp: Long = 0L)
During recovery, the persistent actor is offered a previously saved snapshot via a SnapshotOffer message from which it can initialize internal state.
- var state: Any = _
-
- override def receiveRecover: Receive = {
- case SnapshotOffer(metadata, offeredSnapshot) => state = offeredSnapshot
- case RecoveryCompleted =>
- case event => // ...
- }
The replayed messages that follow the SnapshotOffer message, if any, are younger than the offered snapshot. They finally recover the persistent actor to its current (i.e. latest) state.
In general, a persistent actor is only offered a snapshot if that persistent actor has previously saved one or more snapshots and at least one of these snapshots matches the SnapshotSelectionCriteria that can be specified for recovery.
- override def recovery = Recovery(fromSnapshot = SnapshotSelectionCriteria(
- maxSequenceNr = 457L,
- maxTimestamp = System.currentTimeMillis))
If not specified, they default to SnapshotSelectionCriteria.Latest which selects the latest (= youngest) snapshot. To disable snapshot-based recovery, applications should use SnapshotSelectionCriteria.None. A recovery where no saved snapshot matches the specified SnapshotSelectionCriteria will replay all journaled messages.
Note
In order to use snapshots, a default snapshot-store (akka.persistence.snapshot-store.plugin) must be configured, or the PersistentActor can pick a snapshot store explicitly by overriding defsnapshotPluginId: String.
Since it is acceptable for some applications to not use any snapshotting, it is legal to not configure a snapshot store. However, Akka will log a warning message when this situation is detected and then continue to operate until an actor tries to store a snapshot, at which point the operation will fail (by replying with anSaveSnapshotFailure for example).
Note that Cluster Sharding is using snapshots, so if you use Cluster Sharding you need to define a snapshot store plugin.
Snapshot deletion
A persistent actor can delete individual snapshots by calling the deleteSnapshot method with the sequence number of when the snapshot was taken.
To bulk-delete a range of snapshots matching SnapshotSelectionCriteria, persistent actors should use thedeleteSnapshots method.
Snapshot status handling
Saving or deleting snapshots can either succeed or fail – this information is reported back to the persistent actor via status messages as illustrated in the following table.
| Method | Success | Failure message |
|---|
saveSnapshot(Any) | SaveSnapshotSuccess | SaveSnapshotFailure |
deleteSnapshot(Long) | DeleteSnapshotSuccess | DeleteSnapshotFailure |
deleteSnapshots(SnapshotSelectionCriteria) | DeleteSnapshotsSuccess | DeleteSnapshotsFailure |
If failure messages are left unhandled by the actor, a default warning log message will be logged for each incoming failure message. No default action is performed on the success messages, however you're free to handle them e.g. in order to delete an in memory representation of the snapshot, or in the case of failure to attempt save the snapshot again.
At-Least-Once Delivery
To send messages with at-least-once delivery semantics to destinations you can mix-in AtLeastOnceDelivery trait to your PersistentActor on the sending side. It takes care of re-sending messages when they have not been confirmed within a configurable timeout.
The state of the sending actor, including which messages have been sent that have not been confirmed by the recipient must be persistent so that it can survive a crash of the sending actor or JVM. The AtLeastOnceDelivery trait does not persist anything by itself. It is your responsibility to persist the intent that a message is sent and that a confirmation has been received.
Note
At-least-once delivery implies that original message sending order is not always preserved, and the destination may receive duplicate messages. Semantics do not match those of a normal ActorRef send operation:
- it is not at-most-once delivery
- message order for the same sender–receiver pair is not preserved due to possible resends
- after a crash and restart of the destination messages are still delivered to the new actor incarnation
These semantics are similar to what an ActorPath represents (see Actor Lifecycle), therefore you need to supply a path and not a reference when delivering messages. The messages are sent to the path with an actor selection.
Use the deliver method to send a message to a destination. Call the confirmDelivery method when the destination has replied with a confirmation message.
Relationship between deliver and confirmDelivery
To send messages to the destination path, use the deliver method after you have persisted the intent to send the message.
The destination actor must send back a confirmation message. When the sending actor receives this confirmation message you should persist the fact that the message was delivered successfully and then call the confirmDeliverymethod.
If the persistent actor is not currently recovering, the deliver method will send the message to the destination actor. When recovering, messages will be buffered until they have been confirmed using confirmDelivery. Once recovery has completed, if there are outstanding messages that have not been confirmed (during the message replay), the persistent actor will resend these before sending any other messages.
Deliver requires a deliveryIdToMessage function to pass the provided deliveryId into the message so that the correlation between deliver and confirmDelivery is possible. The deliveryId must do the round trip. Upon receipt of the message, the destination actor will send the same``deliveryId`` wrapped in a confirmation message back to the sender. The sender will then use it to call confirmDelivery method to complete the delivery routine.
- import akka.actor.{ Actor, ActorSelection }
- import akka.persistence.AtLeastOnceDelivery
-
- case class Msg(deliveryId: Long, s: String)
- case class Confirm(deliveryId: Long)
-
- sealed trait Evt
- case class MsgSent(s: String) extends Evt
- case class MsgConfirmed(deliveryId: Long) extends Evt
-
- class MyPersistentActor(destination: ActorSelection)
- extends PersistentActor with AtLeastOnceDelivery {
-
- override def persistenceId: String = "persistence-id"
-
- override def receiveCommand: Receive = {
- case s: String => persist(MsgSent(s))(updateState)
- case Confirm(deliveryId) => persist(MsgConfirmed(deliveryId))(updateState)
- }
-
- override def receiveRecover: Receive = {
- case evt: Evt => updateState(evt)
- }
-
- def updateState(evt: Evt): Unit = evt match {
- case MsgSent(s) =>
- deliver(destination)(deliveryId => Msg(deliveryId, s))
-
- case MsgConfirmed(deliveryId) => confirmDelivery(deliveryId)
- }
- }
-
- class MyDestination extends Actor {
- def receive = {
- case Msg(deliveryId, s) =>
- // ...
- sender() ! Confirm(deliveryId)
- }
- }
The deliveryId generated by the persistence module is a strictly monotonically increasing sequence number without gaps. The same sequence is used for all destinations of the actor, i.e. when sending to multiple destinations the destinations will see gaps in the sequence. It is not possible to use custom deliveryId. However, you can send a custom correlation identifier in the message to the destination. You must then retain a mapping between the internaldeliveryId (passed into the deliveryIdToMessage function) and your custom correlation id (passed into the message). You can do this by storing such mapping in a Map(correlationId -> deliveryId) from which you can retrieve the deliveryId to be passed into the confirmDelivery method once the receiver of your message has replied with your custom correlation id.
The AtLeastOnceDelivery trait has a state consisting of unconfirmed messages and a sequence number. It does not store this state itself. You must persist events corresponding to the deliver and confirmDelivery invocations from your PersistentActor so that the state can be restored by calling the same methods during the recovery phase of thePersistentActor. Sometimes these events can be derived from other business level events, and sometimes you must create separate events. During recovery, calls to deliver will not send out messages, those will be sent later if no matching confirmDelivery will have been performed.
Support for snapshots is provided by getDeliverySnapshot and setDeliverySnapshot. TheAtLeastOnceDeliverySnapshot contains the full delivery state, including unconfirmed messages. If you need a custom snapshot for other parts of the actor state you must also include the AtLeastOnceDeliverySnapshot. It is serialized using protobuf with the ordinary Akka serialization mechanism. It is easiest to include the bytes of theAtLeastOnceDeliverySnapshot as a blob in your custom snapshot.
The interval between redelivery attempts is defined by the redeliverInterval method. The default value can be configured with the akka.persistence.at-least-once-delivery.redeliver-interval configuration key. The method can be overridden by implementation classes to return non-default values.
The maximum number of messages that will be sent at each redelivery burst is defined by the redeliveryBurstLimitmethod (burst frequency is half of the redelivery interval). If there's a lot of unconfirmed messages (e.g. if the destination is not available for a long time), this helps to prevent an overwhelming amount of messages to be sent at once. The default value can be configured with the akka.persistence.at-least-once-delivery.redelivery-burst-limit configuration key. The method can be overridden by implementation classes to return non-default values.
After a number of delivery attempts a AtLeastOnceDelivery.UnconfirmedWarning message will be sent to self. The re-sending will still continue, but you can choose to call confirmDelivery to cancel the re-sending. The number of delivery attempts before emitting the warning is defined by the warnAfterNumberOfUnconfirmedAttemptsmethod. The default value can be configured with the akka.persistence.at-least-once-delivery.warn-after-number-of-unconfirmed-attempts configuration key. The method can be overridden by implementation classes to return non-default values.
The AtLeastOnceDelivery trait holds messages in memory until their successful delivery has been confirmed. The maximum number of unconfirmed messages that the actor is allowed to hold in memory is defined by themaxUnconfirmedMessages method. If this limit is exceed the deliver method will not accept more messages and it will throw AtLeastOnceDelivery.MaxUnconfirmedMessagesExceededException. The default value can be configured with the akka.persistence.at-least-once-delivery.max-unconfirmed-messages configuration key. The method can be overridden by implementation classes to return non-default values.
Event Adapters
In long running projects using event sourcing sometimes the need arises to detach the data model from the domain model completely.
Event Adapters help in situations where:
- Version Migrations – existing events stored in Version 1 should be "upcasted" to a new Version 2 representation, and the process of doing so involves actual code, not just changes on the serialization layer. For these scenarios the
toJournalfunction is usually an identity function, however the fromJournal is implemented as v1.Event=>v2.Event, performing the neccessary mapping inside the fromJournal method. This technique is sometimes refered to as "upcasting" in other CQRS libraries. - Separating Domain and Data models – thanks to EventAdapters it is possible to completely separate the domain model from the model used to persist data in the Journals. For example one may want to use case classes in the domain model, however persist their protocol-buffer (or any other binary serialization format) counter-parts to the Journal. A simple
toJournal:MyModel=>MyDataModel and fromJournal:MyDataModel=>MyModel adapter can be used to implement this feature. - Journal Specialized Data Types – exposing data types understood by the underlying Journal, for example for data stores which understand JSON it is possible to write an EventAdapter
toJournal:Any=>JSON such that the Journal can directlystore the json instead of serializing the object to its binary representation.
Implementing an EventAdapter is rather stright forward:
- class MyEventAdapter(system: ExtendedActorSystem) extends EventAdapter {
- override def manifest(event: Any): String =
- "" // when no manifest needed, return ""
-
- override def toJournal(event: Any): Any =
- event // identity
-
- override def fromJournal(event: Any, manifest: String): EventSeq =
- EventSeq.single(event) // identity
- }
Then in order for it to be used on events coming to and from the journal you must bind it using the below configuration syntax:
- akka.persistence.journal {
- inmem {
- event-adapters {
- tagging = "docs.persistence.MyTaggingEventAdapter"
- user-upcasting = "docs.persistence.UserUpcastingEventAdapter"
- item-upcasting = "docs.persistence.ItemUpcastingEventAdapter"
- }
-
- event-adapter-bindings {
- "docs.persistence.Item" = tagging
- "docs.persistence.TaggedEvent" = tagging
- "docs.persistence.v1.Event" = [user-upcasting, item-upcasting]
- }
- }
- }
It is possible to bind multiple adapters to one class for recovery, in which case the fromJournal methods of all bound adapters will be applied to a given matching event (in order of definition in the configuration). Since each adapter may return from 0 to n adapted events (called as EventSeq), each adapter can investigate the event and if it should indeed adapt it return the adapted event(s) for it. Other adapters which do not have anything to contribute during this adaptation simply return EventSeq.empty. The adapted events are then delivered in-order to the PersistentActorduring replay.
Persistent FSM
PersistentFSM handles the incoming messages in an FSM like fashion. Its internal state is persisted as a sequence of changes, later referred to as domain events. Relationship between incoming messages, FSM's states and transitions, persistence of domain events is defined by a DSL.
Warning
PersistentFSM is marked as “experimental” as of its introduction in Akka 2.4.0. We will continue to improve this API based on our users’ feedback, which implies that while we try to keep incompatible changes to a minimum the binary compatibility guarantee for maintenance releases does not apply to the contents of the classes related to ``PersistentFSM`.
A Simple Example
To demonstrate the features of the PersistentFSM trait, consider an actor which represents a Web store customer. The contract of our "WebStoreCustomerFSMActor" is that it accepts the following commands:
- sealed trait Command
- case class AddItem(item: Item) extends Command
- case object Buy extends Command
- case object Leave extends Command
- case object GetCurrentCart extends Command
AddItem sent when the customer adds an item to a shopping cart Buy - when the customer finishes the purchaseLeave - when the customer leaves the store without purchasing anything GetCurrentCart allows to query the current state of customer's shopping cart
The customer can be in one of the following states:
- sealed trait UserState extends FSMState
- case object LookingAround extends UserState {
- override def identifier: String = "Looking Around"
- }
- case object Shopping extends UserState {
- override def identifier: String = "Shopping"
- }
- case object Inactive extends UserState {
- override def identifier: String = "Inactive"
- }
- case object Paid extends UserState {
- override def identifier: String = "Paid"
- }
LookingAround customer is browsing the site, but hasn't added anything to the shopping cart Shopping customer has recently added items to the shopping cart Inactive customer has items in the shopping cart, but hasn't added anything recently Paid customer has purchased the items
Note
PersistentFSM states must inherit from trait PersistentFSM.FSMState and implement the defidentifier: String method. This is required in order to simplify the serialization of FSM states. String identifiers should be unique!
Customer's actions are "recorded" as a sequence of "domain events" which are persisted. Those events are replayed on an actor's start in order to restore the latest customer's state:
- sealed trait DomainEvent
- case class ItemAdded(item: Item) extends DomainEvent
- case object OrderExecuted extends DomainEvent
- case object OrderDiscarded extends DomainEvent
Customer state data represents the items in a customer's shopping cart:
- case class Item(id: String, name: String, price: Float)
-
- sealed trait ShoppingCart {
- def addItem(item: Item): ShoppingCart
- def empty(): ShoppingCart
- }
- case object EmptyShoppingCart extends ShoppingCart {
- def addItem(item: Item) = NonEmptyShoppingCart(item :: Nil)
- def empty() = this
- }
- case class NonEmptyShoppingCart(items: Seq[Item]) extends ShoppingCart {
- def addItem(item: Item) = NonEmptyShoppingCart(items :+ item)
- def empty() = EmptyShoppingCart
- }
Here is how everything is wired together:
- startWith(LookingAround, EmptyShoppingCart)
-
- when(LookingAround) {
- case Event(AddItem(item), _) ⇒
- goto(Shopping) applying ItemAdded(item) forMax (1 seconds)
- case Event(GetCurrentCart, data) ⇒
- stay replying data
- }
-
- when(Shopping) {
- case Event(AddItem(item), _) ⇒
- stay applying ItemAdded(item) forMax (1 seconds)
- case Event(Buy, _) ⇒
- goto(Paid) applying OrderExecuted andThen {
- case NonEmptyShoppingCart(items) ⇒
- reportActor ! PurchaseWasMade(items)
- saveStateSnapshot()
- case EmptyShoppingCart ⇒ saveStateSnapshot()
- }
- case Event(Leave, _) ⇒
- stop applying OrderDiscarded andThen {
- case _ ⇒
- reportActor ! ShoppingCardDiscarded
- saveStateSnapshot()
- }
- case Event(GetCurrentCart, data) ⇒
- stay replying data
- case Event(StateTimeout, _) ⇒
- goto(Inactive) forMax (2 seconds)
- }
-
- when(Inactive) {
- case Event(AddItem(item), _) ⇒
- goto(Shopping) applying ItemAdded(item) forMax (1 seconds)
- case Event(StateTimeout, _) ⇒
- stop applying OrderDiscarded andThen {
- case _ ⇒ reportActor ! ShoppingCardDiscarded
- }
- }
-
- when(Paid) {
- case Event(Leave, _) ⇒ stop()
- case Event(GetCurrentCart, data) ⇒
- stay replying data
- }
Note
State data can only be modified directly on initialization. Later it's modified only as a result of applying domain events. Override the applyEvent method to define how state data is affected by domain events, see the example below
- override def applyEvent(event: DomainEvent, cartBeforeEvent: ShoppingCart): ShoppingCart = {
- event match {
- case ItemAdded(item) ⇒ cartBeforeEvent.addItem(item)
- case OrderExecuted ⇒ cartBeforeEvent
- case OrderDiscarded ⇒ cartBeforeEvent.empty()
- }
- }
andThen can be used to define actions which will be executed following event's persistence - convenient for "side effects" like sending a message or logging. Notice that actions defined in andThen block are not executed on recovery:
- goto(Paid) applying OrderExecuted andThen {
- case NonEmptyShoppingCart(items) ⇒
- reportActor ! PurchaseWasMade(items)
- }
A snapshot of state data can be persisted by calling the saveStateSnapshot() method:
- stop applying OrderDiscarded andThen {
- case _ ⇒
- reportActor ! ShoppingCardDiscarded
- saveStateSnapshot()
- }
On recovery state data is initialized according to the latest available snapshot, then the remaining domain events are replayed, triggering the applyEvent method.
Storage plugins
Storage backends for journals and snapshot stores are pluggable in the Akka persistence extension.
A directory of persistence journal and snapshot store plugins is available at the Akka Community Projects page, seeCommunity plugins
Plugins can be selected either by "default" for all persistent actors and views, or "individually", when a persistent actor or view defines its own set of plugins.
When a persistent actor or view does NOT override the journalPluginId and snapshotPluginId methods, the persistence extension will use the "default" journal and snapshot-store plugins configured in reference.conf:
- akka.persistence.journal.plugin = ""
- akka.persistence.snapshot-store.plugin = ""
However, these entries are provided as empty "", and require explicit user configuration via override in the userapplication.conf. For an example of a journal plugin which writes messages to LevelDB see Local LevelDB journal. For an example of a snapshot store plugin which writes snapshots as individual files to the local filesystem see Local snapshot store.
Applications can provide their own plugins by implementing a plugin API and activating them by configuration. Plugin development requires the following imports:
- import akka.persistence._
- import akka.persistence.journal._
- import akka.persistence.snapshot._
Eager initialization of persistence plugin
By default, persistence plugins are started on-demand, as they are used. In some case, however, it might be beneficial to start a certain plugin eagerly. In order to do that, you should first add the akka.persistence.Persistence under theakka.extensions key. Then, specify the IDs of plugins you wish to start automatically underakka.persistence.journal.auto-start-journals and akka.persistence.snapshot-store.auto-start-snapshot-stores.
Journal plugin API
A journal plugin extends AsyncWriteJournal.
AsyncWriteJournal is an actor and the methods to be implemented are:
- /**
- * Plugin API: asynchronously writes a batch (`Seq`) of persistent messages to the
- * journal.
- *
- * The batch is only for performance reasons, i.e. all messages don't have to be written
- * atomically. Higher throughput can typically be achieved by using batch inserts of many
- * records compared to inserting records one-by-one, but this aspect depends on the
- * underlying data store and a journal implementation can implement it as efficient as
- * possible. Journals should aim to persist events in-order for a given `persistenceId`
- * as otherwise in case of a failure, the persistent state may be end up being inconsistent.
- *
- * Each `AtomicWrite` message contains the single `PersistentRepr` that corresponds to
- * the event that was passed to the `persist` method of the `PersistentActor`, or it
- * contains several `PersistentRepr` that corresponds to the events that were passed
- * to the `persistAll` method of the `PersistentActor`. All `PersistentRepr` of the
- * `AtomicWrite` must be written to the data store atomically, i.e. all or none must
- * be stored. If the journal (data store) cannot support atomic writes of multiple
- * events it should reject such writes with a `Try` `Failure` with an
- * `UnsupportedOperationException` describing the issue. This limitation should
- * also be documented by the journal plugin.
- *
- * If there are failures when storing any of the messages in the batch the returned
- * `Future` must be completed with failure. The `Future` must only be completed with
- * success when all messages in the batch have been confirmed to be stored successfully,
- * i.e. they will be readable, and visible, in a subsequent replay. If there is
- * uncertainty about if the messages were stored or not the `Future` must be completed
- * with failure.
- *
- * Data store connection problems must be signaled by completing the `Future` with
- * failure.
- *
- * The journal can also signal that it rejects individual messages (`AtomicWrite`) by
- * the returned `immutable.Seq[Try[Unit]]`. It is possible but not mandatory to reduce
- * number of allocations by returning `Future.successful(Nil)` for the happy path,
- * i.e. when no messages are rejected. Otherwise the returned `Seq` must have as many elements
- * as the input `messages` `Seq`. Each `Try` element signals if the corresponding
- * `AtomicWrite` is rejected or not, with an exception describing the problem. Rejecting
- * a message means it was not stored, i.e. it must not be included in a later replay.
- * Rejecting a message is typically done before attempting to store it, e.g. because of
- * serialization error.
- *
- * Data store connection problems must not be signaled as rejections.
- *
- * It is possible but not mandatory to reduce number of allocations by returning
- * `Future.successful(Nil)` for the happy path, i.e. when no messages are rejected.
- *
- * Calls to this method are serialized by the enclosing journal actor. If you spawn
- * work in asynchronous tasks it is alright that they complete the futures in any order,
- * but the actual writes for a specific persistenceId should be serialized to avoid
- * issues such as events of a later write are visible to consumers (query side, or replay)
- * before the events of an earlier write are visible.
- * A PersistentActor will not send a new WriteMessages request before the previous one
- * has been completed.
- *
- * Please note that the `sender` field of the contained PersistentRepr objects has been
- * nulled out (i.e. set to `ActorRef.noSender`) in order to not use space in the journal
- * for a sender reference that will likely be obsolete during replay.
- *
- * Please also note that requests for the highest sequence number may be made concurrently
- * to this call executing for the same `persistenceId`, in particular it is possible that
- * a restarting actor tries to recover before its outstanding writes have completed. In
- * the latter case it is highly desirable to defer reading the highest sequence number
- * until all outstanding writes have completed, otherwise the PersistentActor may reuse
- * sequence numbers.
- *
- * This call is protected with a circuit-breaker.
- */
- def asyncWriteMessages(messages: immutable.Seq[AtomicWrite]): Future[immutable.Seq[Try[Unit]]]
-
- /**
- * Plugin API: asynchronously deletes all persistent messages up to `toSequenceNr`
- * (inclusive).
- *
- * This call is protected with a circuit-breaker.
- * Message deletion doesn't affect the highest sequence number of messages, journal must maintain the highest sequence number and never decrease it.
- */
- def asyncDeleteMessagesTo(persistenceId: String, toSequenceNr: Long): Future[Unit]
-
- /**
- * Plugin API
- *
- * Allows plugin implementers to use `f pipeTo self` and
- * handle additional messages for implementing advanced features
- *
- */
- def receivePluginInternal: Actor.Receive = Actor.emptyBehavior
If the storage backend API only supports synchronous, blocking writes, the methods should be implemented as:
- def asyncWriteMessages(messages: immutable.Seq[AtomicWrite]): Future[immutable.Seq[Try[Unit]]] =
- Future.fromTry(Try {
- // blocking call here
- ???
- })
A journal plugin must also implement the methods defined in AsyncRecovery for replays and sequence number recovery:
- /**
- * Plugin API: asynchronously replays persistent messages. Implementations replay
- * a message by calling `replayCallback`. The returned future must be completed
- * when all messages (matching the sequence number bounds) have been replayed.
- * The future must be completed with a failure if any of the persistent messages
- * could not be replayed.
- *
- * The `replayCallback` must also be called with messages that have been marked
- * as deleted. In this case a replayed message's `deleted` method must return
- * `true`.
- *
- * The `toSequenceNr` is the lowest of what was returned by [[#asyncReadHighestSequenceNr]]
- * and what the user specified as recovery [[akka.persistence.Recovery]] parameter.
- * This does imply that this call is always preceded by reading the highest sequence
- * number for the given `persistenceId`.
- *
- * This call is NOT protected with a circuit-breaker because it may take long time
- * to replay all events. The plugin implementation itself must protect against
- * an unresponsive backend store and make sure that the returned Future is
- * completed with success or failure within reasonable time. It is not allowed
- * to ignore completing the future.
- *
- * @param persistenceId persistent actor id.
- * @param fromSequenceNr sequence number where replay should start (inclusive).
- * @param toSequenceNr sequence number where replay should end (inclusive).
- * @param max maximum number of messages to be replayed.
- * @param recoveryCallback called to replay a single message. Can be called from any
- * thread.
- *
- * @see [[AsyncWriteJournal]]
- */
- def asyncReplayMessages(persistenceId: String, fromSequenceNr: Long, toSequenceNr: Long,
- max: Long)(recoveryCallback: PersistentRepr ⇒ Unit): Future[Unit]
-
- /**
- * Plugin API: asynchronously reads the highest stored sequence number for the
- * given `persistenceId`. The persistent actor will use the highest sequence
- * number after recovery as the starting point when persisting new events.
- * This sequence number is also used as `toSequenceNr` in subsequent call
- * to [[#asyncReplayMessages]] unless the user has specified a lower `toSequenceNr`.
- * Journal must maintain the highest sequence number and never decrease it.
- *
- * This call is protected with a circuit-breaker.
- *
- * Please also note that requests for the highest sequence number may be made concurrently
- * to writes executing for the same `persistenceId`, in particular it is possible that
- * a restarting actor tries to recover before its outstanding writes have completed.
- *
- * @param persistenceId persistent actor id.
- * @param fromSequenceNr hint where to start searching for the highest sequence
- * number. When a persistent actor is recovering this
- * `fromSequenceNr` will be the sequence number of the used
- * snapshot or `0L` if no snapshot is used.
- */
- def asyncReadHighestSequenceNr(persistenceId: String, fromSequenceNr: Long): Future[Long]
A journal plugin can be activated with the following minimal configuration:
- # Path to the journal plugin to be used
- akka.persistence.journal.plugin = "my-journal"
-
- # My custom journal plugin
- my-journal {
- # Class name of the plugin.
- class = "docs.persistence.MyJournal"
- # Dispatcher for the plugin actor.
- plugin-dispatcher = "akka.actor.default-dispatcher"
- }
The specified plugin class must have a no-arg constructor. The plugin-dispatcher is the dispatcher used for the plugin actor. If not specified, it defaults to akka.persistence.dispatchers.default-plugin-dispatcher.
The journal plugin instance is an actor so the methods corresponding to requests from persistent actors are executed sequentially. It may delegate to asynchronous libraries, spawn futures, or delegate to other actors to achive parallelism.
The journal plugin class must have a constructor without parameters or a constructor with onecom.typesafe.config.Config parameter. The plugin section of the actor system's config will be passed in the config constructor parameter.
Don't run journal tasks/futures on the system default dispatcher, since that might starve other tasks.
Snapshot store plugin API
A snapshot store plugin must extend the SnapshotStore actor and implement the following methods:
- /**
- * Plugin API: asynchronously loads a snapshot.
- *
- * This call is protected with a circuit-breaker.
- *
- * @param persistenceId id of the persistent actor.
- * @param criteria selection criteria for loading.
- */
- def loadAsync(persistenceId: String, criteria: SnapshotSelectionCriteria): Future[Option[SelectedSnapshot]]
-
- /**
- * Plugin API: asynchronously saves a snapshot.
- *
- * This call is protected with a circuit-breaker.
- *
- * @param metadata snapshot metadata.
- * @param snapshot snapshot.
- */
- def saveAsync(metadata: SnapshotMetadata, snapshot: Any): Future[Unit]
-
- /**
- * Plugin API: deletes the snapshot identified by `metadata`.
- *
- * This call is protected with a circuit-breaker.
- *
- * @param metadata snapshot metadata.
- */
- def deleteAsync(metadata: SnapshotMetadata): Future[Unit]
-
- /**
- * Plugin API: deletes all snapshots matching `criteria`.
- *
- * This call is protected with a circuit-breaker.
- *
- * @param persistenceId id of the persistent actor.
- * @param criteria selection criteria for deleting.
- */
- def deleteAsync(persistenceId: String, criteria: SnapshotSelectionCriteria): Future[Unit]
-
- /**
- * Plugin API
- * Allows plugin implementers to use `f pipeTo self` and
- * handle additional messages for implementing advanced features
- */
- def receivePluginInternal: Actor.Receive = Actor.emptyBehavior
A snapshot store plugin can be activated with the following minimal configuration:
- # Path to the snapshot store plugin to be used
- akka.persistence.snapshot-store.plugin = "my-snapshot-store"
-
- # My custom snapshot store plugin
- my-snapshot-store {
- # Class name of the plugin.
- class = "docs.persistence.MySnapshotStore"
- # Dispatcher for the plugin actor.
- plugin-dispatcher = "akka.persistence.dispatchers.default-plugin-dispatcher"
- }
The specified plugin class must have a no-arg constructor. The plugin-dispatcher is the dispatcher used for the plugin actor. If not specified, it defaults to akka.persistence.dispatchers.default-plugin-dispatcher.
The snapshot store instance is an actor so the methods corresponding to requests from persistent actors are executed sequentially. It may delegate to asynchronous libraries, spawn futures, or delegate to other actors to achive parallelism.
The snapshot store plugin class must have a constructor without parameters or a constructor with onecom.typesafe.config.Config parameter. The plugin section of the actor system's config will be passed in the config constructor parameter.
Don't run snapshot store tasks/futures on the system default dispatcher, since that might starve other tasks.
Plugin TCK
In order to help developers build correct and high quality storage plugins, we provide a Technology Compatibility Kit (TCK for short).
The TCK is usable from Java as well as Scala projects. For Scala you need to include the akka-persistence-tck dependency:
- "com.typesafe.akka" %% "akka-persistence-tck" % "2.4.9" % "test"
To include the Journal TCK tests in your test suite simply extend the provided JournalSpec:
- class MyJournalSpec extends JournalSpec(
- config = ConfigFactory.parseString(
- """akka.persistence.journal.plugin = "my.journal.plugin"""")) {
-
- override def supportsRejectingNonSerializableObjects: CapabilityFlag =
- false // or CapabilityFlag.off
- }
Please note that some of the tests are optional, and by overriding the supports... methods you give the TCK the needed information about which tests to run. You can implement these methods using boolean falues or the providedCapabilityFlag.on / CapabilityFlag.off values.
We also provide a simple benchmarking class JournalPerfSpec which includes all the tests that JournalSpec has, and also performs some longer operations on the Journal while printing its performance stats. While it is NOT aimed to provide a proper benchmarking environment it can be used to get a rough feel about your journal's performance in the most typical scenarios.
In order to include the SnapshotStore TCK tests in your test suite simply extend the SnapshotStoreSpec:
- class MySnapshotStoreSpec extends SnapshotStoreSpec(
- config = ConfigFactory.parseString(
- """
- akka.persistence.snapshot-store.plugin = "my.snapshot-store.plugin"
- """))
In case your plugin requires some setting up (starting a mock database, removing temporary files etc.) you can override the beforeAll and afterAll methods to hook into the tests lifecycle:
- class MyJournalSpec extends JournalSpec(
- config = ConfigFactory.parseString(
- """
- akka.persistence.journal.plugin = "my.journal.plugin"
- """)) {
-
- override def supportsRejectingNonSerializableObjects: CapabilityFlag =
- true // or CapabilityFlag.on
-
- val storageLocations = List(
- new File(system.settings.config.getString("akka.persistence.journal.leveldb.dir")),
- new File(config.getString("akka.persistence.snapshot-store.local.dir")))
-
- override def beforeAll() {
- super.beforeAll()
- storageLocations foreach FileUtils.deleteRecursively
- }
-
- override def afterAll() {
- storageLocations foreach FileUtils.deleteRecursively
- super.afterAll()
- }
-
- }
We highly recommend including these specifications in your test suite, as they cover a broad range of cases you might have otherwise forgotten to test for when writing a plugin from scratch.
Pre-packaged plugins
Local LevelDB journal
The LevelDB journal plugin config entry is akka.persistence.journal.leveldb. It writes messages to a local LevelDB instance. Enable this plugin by defining config property:
- # Path to the journal plugin to be used
- akka.persistence.journal.plugin = "akka.persistence.journal.leveldb"
LevelDB based plugins will also require the following additional dependency declaration:
- "org.iq80.leveldb" % "leveldb" % "0.7"
- "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8"
The default location of LevelDB files is a directory named journal in the current working directory. This location can be changed by configuration where the specified path can be relative or absolute:
- akka.persistence.journal.leveldb.dir = "target/journal"
With this plugin, each actor system runs its own private LevelDB instance.
Shared LevelDB journal
A LevelDB instance can also be shared by multiple actor systems (on the same or on different nodes). This, for example, allows persistent actors to failover to a backup node and continue using the shared journal instance from the backup node.
Warning
A shared LevelDB instance is a single point of failure and should therefore only be used for testing purposes. Highly-available, replicated journals are available as Community plugins.
A shared LevelDB instance is started by instantiating the SharedLeveldbStore actor.
- import akka.persistence.journal.leveldb.SharedLeveldbStore
-
- val store = system.actorOf(Props[SharedLeveldbStore], "store")
By default, the shared instance writes journaled messages to a local directory named journal in the current working directory. The storage location can be changed by configuration:
- akka.persistence.journal.leveldb-shared.store.dir = "target/shared"
Actor systems that use a shared LevelDB store must activate the akka.persistence.journal.leveldb-sharedplugin.
- akka.persistence.journal.plugin = "akka.persistence.journal.leveldb-shared"
This plugin must be initialized by injecting the (remote) SharedLeveldbStore actor reference. Injection is done by calling the SharedLeveldbJournal.setStore method with the actor reference as argument.
- trait SharedStoreUsage extends Actor {
- override def preStart(): Unit = {
- context.actorSelection("akka.tcp://example@127.0.0.1:2552/user/store") ! Identify(1)
- }
-
- def receive = {
- case ActorIdentity(1, Some(store)) =>
- SharedLeveldbJournal.setStore(store, context.system)
- }
- }
Internal journal commands (sent by persistent actors) are buffered until injection completes. Injection is idempotent i.e. only the first injection is used.
Local snapshot store
The local snapshot store plugin config entry is akka.persistence.snapshot-store.local. It writes snapshot files to the local filesystem. Enable this plugin by defining config property:
- # Path to the snapshot store plugin to be used
- akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local"
The default storage location is a directory named snapshots in the current working directory. This can be changed by configuration where the specified path can be relative or absolute:
- akka.persistence.snapshot-store.local.dir = "target/snapshots"
Note that it is not mandatory to specify a snapshot store plugin. If you don't use snapshots you don't have to configure it.
Persistence Plugin Proxy
A persistence plugin proxy allows sharing of journals and snapshot stores across multiple actor systems (on the same or on different nodes). This, for example, allows persistent actors to failover to a backup node and continue using the shared journal instance from the backup node. The proxy works by forwarding all the journal/snapshot store messages to a single, shared, persistence plugin instance, and therefor supports any use case supported by the proxied plugin.
Warning
A shared journal/snapshot store is a single point of failure and should therefore only be used for testing purposes. Highly-available, replicated persistence plugins are available as Community plugins.
The journal and snapshot store proxies are controlled via the akka.persistence.journal.proxy andakka.persistence.snapshot-store.proxy configuration entries, respectively. Set the target-journal-pluginor target-snapshot-store-plugin keys to the underlying plugin you wish to use (for example:akka.persistence.journal.leveldb). The start-target-journal and start-target-snapshot-store keys should be set to on in exactly one actor system - this is the system that will instantiate the shared persistence plugin. Next, the proxy needs to be told how to find the shared plugin. This can be done by setting the target-journal-address and target-snapshot-store-address configuration keys, or programmatically by calling thePersistencePluginProxy.setTargetLocation method.
Note
Akka starts extensions lazily when they are required, and this includes the proxy. This means that in order for the proxy to work, the persistence plugin on the target node must be instantiated. This can be done by instantiating the PersistencePluginProxyExtension extension, or by calling the PersistencePluginProxy.startmethod.
Note
The proxied persistence plugin can (and should) be configured using its original configuration keys.
Custom serialization
Serialization of snapshots and payloads of Persistent messages is configurable with Akka's Serializationinfrastructure. For example, if an application wants to serialize
- payloads of type
MyPayload with a custom MyPayloadSerializer and - snapshots of type
MySnapshot with a custom MySnapshotSerializer
it must add
- akka.actor {
- serializers {
- my-payload = "docs.persistence.MyPayloadSerializer"
- my-snapshot = "docs.persistence.MySnapshotSerializer"
- }
- serialization-bindings {
- "docs.persistence.MyPayload" = my-payload
- "docs.persistence.MySnapshot" = my-snapshot
- }
- }
to the application configuration. If not specified, a default serializer is used.
For more advanced schema evolution techniques refer to the Persistence - Schema Evolution documentation.
Testing
When running tests with LevelDB default settings in sbt, make sure to set fork := true in your sbt project. Otherwise, you'll see an UnsatisfiedLinkError. Alternatively, you can switch to a LevelDB Java port by setting
- akka.persistence.journal.leveldb.native = off
or
- akka.persistence.journal.leveldb-shared.store.native = off
in your Akka configuration. The LevelDB Java port is for testing purposes only.
Warning
It is not possible to test persistence provided classes (i.e. PersistentActor and AtLeastOnceDelivery) usingTestActorRef due to its synchronous nature. These traits need to be able to perform asynchronous tasks in the background in order to handle internal persistence related events.
When testing Persistence based projects always rely on asynchronous messaging using the TestKit.
Configuration
There are several configuration properties for the persistence module, please refer to the reference configuration.
Multiple persistence plugin configurations
By default, a persistent actor or view will use the "default" journal and snapshot store plugins configured in the following sections of the reference.conf configuration resource:
- # Absolute path to the default journal plugin configuration entry.
- akka.persistence.journal.plugin = "akka.persistence.journal.inmem"
- # Absolute path to the default snapshot store plugin configuration entry.
- akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local"
Note that in this case the actor or view overrides only the persistenceId method:
- trait ActorWithDefaultPlugins extends PersistentActor {
- override def persistenceId = "123"
- }
When the persistent actor or view overrides the journalPluginId and snapshotPluginId methods, the actor or view will be serviced by these specific persistence plugins instead of the defaults:
- trait ActorWithOverridePlugins extends PersistentActor {
- override def persistenceId = "123"
- // Absolute path to the journal plugin configuration entry in the `reference.conf`.
- override def journalPluginId = "akka.persistence.chronicle.journal"
- // Absolute path to the snapshot store plugin configuration entry in the `reference.conf`.
- override def snapshotPluginId = "akka.persistence.chronicle.snapshot-store"
- }
Note that journalPluginId and snapshotPluginId must refer to properly configured reference.conf plugin entries with a standard class property as well as settings which are specific for those plugins, i.e.:
- # Configuration entry for the custom journal plugin, see `journalPluginId`.
- akka.persistence.chronicle.journal {
- # Standard persistence extension property: provider FQCN.
- class = "akka.persistence.chronicle.ChronicleSyncJournal"
- # Custom setting specific for the journal `ChronicleSyncJournal`.
- folder = $${user.dir}/store/journal
- }
- # Configuration entry for the custom snapshot store plugin, see `snapshotPluginId`.
- akka.persistence.chronicle.snapshot-store {
- # Standard persistence extension property: provider FQCN.
- class = "akka.persistence.chronicle.ChronicleSnapshotStore"
- # Custom setting specific for the snapshot store `ChronicleSnapshotStore`.
- folder = $${user.dir}/store/snapshot
- }
Persistence - Schema Evolution
When working on long running projects using Persistence, or any kind of Event Sourcing architectures, schema evolution becomes one of the more important technical aspects of developing your application. The requirements as well as our own understanding of the business domain may (and will) change in time.
In fact, if a project matures to the point where you need to evolve its schema to adapt to changing business requirements you can view this as first signs of its success – if you wouldn't need to adapt anything over an apps lifecycle that could mean that no-one is really using it actively.
In this chapter we will investigate various schema evolution strategies and techniques from which you can pick and choose the ones that match your domain and challenge at hand.
Note
This page proposes a number of possible solutions to the schema evolution problem and explains how some of the utilities Akka provides can be used to achieve this, it is by no means a complete (closed) set of solutions.
Sometimes, based on the capabilities of your serialization formats, you may be able to evolve your schema in different ways than outlined in the sections below. If you discover useful patterns or techniques for schema evolution feel free to submit Pull Requests to this page to extend it.
Schema evolution in event-sourced systems
In recent years we have observed a tremendous move towards immutable append-only datastores, with event-sourcing being the prime technique successfully being used in these settings. For an excellent overview why and how immutable data makes scalability and systems design much simpler you may want to read Pat Helland's excellent Immutability Changes Everything whitepaper.
Since with Event Sourcing the events are immutable and usually never deleted – the way schema evolution is handled differs from how one would go about it in a mutable database setting (e.g. in typical CRUD database applications).
The system needs to be able to continue to work in the presence of "old" events which were stored under the "old" schema. We also want to limit complexity in the business logic layer, exposing a consistent view over all of the events of a given type to PersistentActor s and persistence queries. This allows the business logic layer to focus on solving business problems instead of having to explicitly deal with different schemas.
- In summary, schema evolution in event sourced systems exposes the following characteristics:
- Allow the system to continue operating without large scale migrations to be applied,
- Allow the system to read "old" events from the underlying storage, however present them in a "new" view to the application logic,
- Transparently promote events to the latest versions during recovery (or queries) such that the business logic need not consider multiple versions of events
Types of schema evolution
Before we explain the various techniques that can be used to safely evolve the schema of your persistent events over time, we first need to define what the actual problem is, and what the typical styles of changes are.
Since events are never deleted, we need to have a way to be able to replay (read) old events, in such way that does not force the PersistentActor to be aware of all possible versions of an event that it may have persisted in the past. Instead, we want the Actors to work on some form of "latest" version of the event and provide some means of either converting old "versions" of stored events into this "latest" event type, or constantly evolve the event definition - in a backwards compatible way - such that the new deserialization code can still read old events.
The most common schema changes you will likely are:
The following sections will explain some patterns which can be used to safely evolve your schema when facing those changes.
Picking the serialization format is a very important decision you will have to make while building your application. It affects which kind of evolutions are simple (or hard) to do, how much work is required to add a new datatype, and, last but not least, serialization performance.
If you find yourself realising you have picked "the wrong" serialization format, it is always possible to change the format used for storing new events, however you would have to keep the old deserialization code in order to be able to replay events that were persisted using the old serialization scheme. It is possible to "rebuild" an event-log from one serialization format to another one, however it may be a more involved process if you need to perform this on a live system.
Binary serialization formats that we have seen work well for long-lived applications include the very flexible IDL based:Google Protobuf, Apache Thrift or Apache Avro. Avro schema evolution is more "entire schema" based, instead of single fields focused like in protobuf or thrift, and usually requires using some kind of schema registry.
Users who want their data to be human-readable directly in the write-side datastore may opt to use plain-old JSON as the storage format, though that comes at a cost of lacking support for schema evolution and relatively large marshalling latency.
There are plenty excellent blog posts explaining the various trade-offs between popular serialization formats, one post we would like to highlight is the very well illustrated Schema evolution in Avro, Protocol Buffers and Thrift by Martin Kleppmann.
Provided default serializers
Akka Persistence provides Google Protocol Buffers based serializers (using Akka Serialization) for it's own message types such as PersistentRepr, AtomicWrite and snapshots. Journal plugin implementations may choose to use those provided serializers, or pick a serializer which suits the underlying database better.
Note
Serialization is NOT handled automatically by Akka Persistence itself. Instead, it only provides the above described serializers, and in case a AsyncWriteJournal plugin implementation chooses to use them directly, the above serialization scheme will be used.
Please refer to your write journal's documentation to learn more about how it handles serialization!
For example, some journals may choose to not use Akka Serialization at all and instead store the data in a format that is more "native" for the underlying datastore, e.g. using JSON or some other kind of format that the target datastore understands directly.
The below figure explains how the default serialization scheme works, and how it fits together with serializing the user provided message itself, which we will from here on refer to as the payload (highlighted in yellow):
The blue colored regions of the PersistentMessage indicate what is serialized using the generated protocol buffers serializers, and the yellow payload indicates the user provided event (by calling persist(payload)(...)). As you can see, the PersistentMessage acts as an envelope around the payload, adding various fields related to the origin of the event (persistenceId, sequenceNr and more).
More advanced techniques (e.g. Remove event class and ignore events) will dive into using the manifests for increasing the flexibility of the persisted vs. exposed types even more. However for now we will focus on the simpler evolution techniques, concerning simply configuring the payload serializers.
By default the payload will be serialized using Java Serialization. This is fine for testing and initial phases of your development (while you're still figuring out things and the data will not need to stay persisted forever). However, once you move to production you should really pick a different serializer for your payloads.
Warning
Do not rely on Java serialization (which will be picked by Akka by default if you don't specify any serializers) forserious application development! It does not lean itself well to evolving schemas over long periods of time, and its performance is also not very high (it never was designed for high-throughput scenarios).
Configuring payload serializers
This section aims to highlight the complete basics on how to define custom serializers using Akka Serialization. Many journal plugin implementations use Akka Serialization, thus it is tremendously important to understand how to configure it to work with your event classes.
Note
Read the Akka Serialization docs to learn more about defining custom serializers, to improve performance and maintainability of your system. Do not depend on Java serialization for production deployments.
The below snippet explains in the minimal amount of lines how a custom serializer can be registered. For more in-depth explanations on how serialization picks the serializer to use etc, please refer to its documentation.
First we start by defining our domain model class, here representing a person:
- final case class Person(name: String, surname: String)
Next we implement a serializer (or extend an existing one to be able to handle the new Person class):
- /**
- * Simplest possible serializer, uses a string representation of the Person class.
- *
- * Usually a serializer like this would use a library like:
- * protobuf, kryo, avro, cap'n proto, flatbuffers, SBE or some other dedicated serializer backend
- * to perform the actual to/from bytes marshalling.
- */
- class SimplestPossiblePersonSerializer extends SerializerWithStringManifest {
- val Utf8 = Charset.forName("UTF-8")
-
- val PersonManifest = classOf[Person].getName
-
- // unique identifier of the serializer
- def identifier = 1234567
-
- // extract manifest to be stored together with serialized object
- override def manifest(o: AnyRef): String = o.getClass.getName
-
- // serialize the object
- override def toBinary(obj: AnyRef): Array[Byte] = obj match {
- case p: Person => s"""${p.name}|${p.surname}""".getBytes(Utf8)
- case _ => throw new IllegalArgumentException(
- s"Unable to serialize to bytes, clazz was: ${obj.getClass}!")
- }
-
- // deserialize the object, using the manifest to indicate which logic to apply
- override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef =
- manifest match {
- case PersonManifest =>
- val nameAndSurname = new String(bytes, Utf8)
- val Array(name, surname) = nameAndSurname.split("[|]")
- Person(name, surname)
- case _ => throw new IllegalArgumentException(
- s"Unable to deserialize from bytes, manifest was: $manifest! Bytes length: " +
- bytes.length)
- }
-
- }
And finally we register the serializer and bind it to handle the docs.persistence.Person class:
- # application.conf
- akka {
- actor {
- serializers {
- person = "docs.persistence.SimplestPossiblePersonSerializer"
- }
-
- serialization-bindings {
- "docs.persistence.Person" = person
- }
- }
- }
Deserialization will be performed by the same serializer which serialized the message initially because of theidentifier being stored together with the message.
Please refer to the Akka Serialization documentation for more advanced use of serializers, especially the Serializer with String Manifest section since it is very useful for Persistence based applications dealing with schema evolutions, as we will see in some of the examples below.
Schema evolution in action
In this section we will discuss various schema evolution techniques using concrete examples and explaining some of the various options one might go about handling the described situation. The list below is by no means a complete guide, so feel free to adapt these techniques depending on your serializer's capabilities and/or other domain specific limitations.
Add fields
Situation: You need to add a field to an existing message type. For example, a SeatReservation(letter:String,row:Int) now needs to have an associated code which indicates if it is a window or aisle seat.
Solution: Adding fields is the most common change you'll need to apply to your messages so make sure the serialization format you picked for your payloads can handle it apropriately, i.e. such changes should be binary compatible. This is easily achieved using the right serializer toolkit – we recommend something like Google Protocol Buffers or Apache Thrift however other tools may fit your needs just as well – picking a serializer backend is something you should research before picking one to run with. In the following examples we will be using protobuf, mostly because we are familiar with it, it does its job well and Akka is using it internally as well.
While being able to read messages with missing fields is half of the solution, you also need to deal with the missing values somehow. This is usually modeled as some kind of default value, or by representing the field as an Option[T]See below for an example how reading an optional field from a serialized protocol buffers message might look like.
- sealed abstract class SeatType { def code: String }
- object SeatType {
- def fromString(s: String) = s match {
- case Window.code => Window
- case Aisle.code => Aisle
- case Other.code => Other
- case _ => Unknown
- }
- case object Window extends SeatType { override val code = "W" }
- case object Aisle extends SeatType { override val code = "A" }
- case object Other extends SeatType { override val code = "O" }
- case object Unknown extends SeatType { override val code = "" }
-
- }
-
- case class SeatReserved(letter: String, row: Int, seatType: SeatType)
Next we prepare an protocol definition using the protobuf Interface Description Language, which we'll use to generate the serializer code to be used on the Akka Serialization layer (notice that the schema aproach allows us to easily rename fields, as long as the numeric identifiers of the fields do not change):
- // FlightAppModels.proto
- option java_package = "docs.persistence.proto";
- option optimize_for = SPEED;
-
- message SeatReserved {
- required string letter = 1;
- required uint32 row = 2;
- optional string seatType = 3; // the new field
- }
The serializer implementation uses the protobuf generated classes to marshall the payloads. Optional fields can be handled explicitly or missing values by calling the has... methods on the protobuf object, which we do forseatType in order to use a Unknown type in case the event was stored before we had introduced the field to this event type:
- /**
- * Example serializer impl which uses protocol buffers generated classes (proto.*)
- * to perform the to/from binary marshalling.
- */
- class AddedFieldsSerializerWithProtobuf extends SerializerWithStringManifest {
- override def identifier = 67876
-
- final val SeatReservedManifest = classOf[SeatReserved].getName
-
- override def manifest(o: AnyRef): String = o.getClass.getName
-
- override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef =
- manifest match {
- case SeatReservedManifest =>
- // use generated protobuf serializer
- seatReserved(FlightAppModels.SeatReserved.parseFrom(bytes))
- case _ =>
- throw new IllegalArgumentException("Unable to handle manifest: " + manifest)
- }
-
- override def toBinary(o: AnyRef): Array[Byte] = o match {
- case s: SeatReserved =>
- FlightAppModels.SeatReserved.newBuilder
- .setRow(s.row)
- .setLetter(s.letter)
- .setSeatType(s.seatType.code)
- .build().toByteArray
- }
-
- // -- fromBinary helpers --
-
- private def seatReserved(p: FlightAppModels.SeatReserved): SeatReserved =
- SeatReserved(p.getLetter, p.getRow, seatType(p))
-
- // handle missing field by assigning "Unknown" value
- private def seatType(p: FlightAppModels.SeatReserved): SeatType =
- if (p.hasSeatType) SeatType.fromString(p.getSeatType) else SeatType.Unknown
-
- }
Rename fields
Situation: When first designing the system the SeatReverved event featured an code field. After some time you discover that what was originally called code actually means seatNr, thus the model should be changed to reflect this concept more accurately.
Solution 1 - using IDL based serializers: First, we will discuss the most efficient way of dealing with such kinds of schema changes – IDL based serializers.
IDL stands for Interface Description Language, and means that the schema of the messages that will be stored is based on this description. Most IDL based serializers also generate the serializer / deserializer code so that using them is not too hard. Examples of such serializers are protobuf or thrift.
Using these libraries rename operations are "free", because the field name is never actually stored in the binary representation of the message. This is one of the advantages of schema based serializers, even though that they add the overhead of having to maintain the schema. When using serializers like this, no additional code change (except renaming the field and method used during serialization) is needed to perform such evolution:
This is how such a rename would look in protobuf:
- // protobuf message definition, BEFORE:
- message SeatReserved {
- required string code = 1;
- }
-
- // protobuf message definition, AFTER:
- message SeatReserved {
- required string seatNr = 1; // field renamed, id remains the same
- }
It is important to learn about the strengths and limitations of your serializers, in order to be able to move swiftly and refactor your models fearlessly as you go on with the project.
Note
Learn in-depth about the serialization engine you're using as it will impact how you can aproach schema evolution.
Some operations are "free" in certain serialization formats (more often than not: removing/adding optional fields, sometimes renaming fields etc.), while some other operations are strictly not possible.
Solution 2 - by manually handling the event versions: Another solution, in case your serialization format does not support renames as easily as the above mentioned formats, is versioning your schema. For example, you could have made your events carry an additional field called _version which was set to 1 (because it was the initial schema), and once you change the schema you bump this number to 2, and write an adapter which can perform the rename.
This approach is popular when your serialization format is something like JSON, where renames can not be performed automatically by the serializer. You can do these kinds of "promotions" either manually (as shown in the example below) or using a library like Stamina which helps to create those V1->V2->V3->...->Vn promotion chains without much boilerplate.
The following snippet showcases how one could apply renames if working with plain JSON (usingspray.json.JsObject):
- class JsonRenamedFieldAdapter extends EventAdapter {
- val marshaller = new ExampleJsonMarshaller
-
- val V1 = "v1"
- val V2 = "v2"
-
- // this could be done independently for each event type
- override def manifest(event: Any): String = V2
-
- override def toJournal(event: Any): JsObject =
- marshaller.toJson(event)
-
- override def fromJournal(event: Any, manifest: String): EventSeq = event match {
- case json: JsObject => EventSeq(marshaller.fromJson(manifest match {
- case V1 => rename(json, "code", "seatNr")
- case V2 => json // pass-through
- case unknown => throw new IllegalArgumentException(s"Unknown manifest: $unknown")
- }))
- case _ =>
- val c = event.getClass
- throw new IllegalArgumentException("Can only work with JSON, was: %s".format(c))
- }
-
- def rename(json: JsObject, from: String, to: String): JsObject = {
- val value = json.fields(from)
- val withoutOld = json.fields - from
- JsObject(withoutOld + (to -> value))
- }
-
- }
As you can see, manually handling renames induces some boilerplate onto the EventAdapter, however much of it you will find is common infrastructure code that can be either provided by an external library (for promotion management) or put together in a simple helper trait.
Note
The technique of versioning events and then promoting them to the latest version using JSON transformations can of course be applied to more than just field renames – it also applies to adding fields and all kinds of changes in the message format.
Remove event class and ignore events
Situation: While investigating app performance you notice that insane amounts of CustomerBlinked events are being stored for every customer each time he/she blinks. Upon investigation you decide that the event does not add any value and should be deleted. You still have to be able to replay from a journal which contains those old CustomerBlinked events though.
Naive solution - drop events in EventAdapter:
The problem of removing an event type from the domain model is not as much its removal, as the implications for the recovery mechanisms that this entails. For example, a naive way of filtering out certain kinds of events from being delivered to a recovering PersistentActor is pretty simple, as one can simply filter them out in an EventAdapter:
This however does not address the underlying cost of having to deserialize all the events during recovery, even those which will be filtered out by the adapter. In the next section we will improve the above explained mechanism to avoid deserializing events which would be filtered out by the adapter anyway, thus allowing to save precious time during a recovery containing lots of such events (without actually having to delete them).
Improved solution - deserialize into tombstone:
In the just described technique we have saved the PersistentActor from receiving un-wanted events by filtering them out in the EventAdapter, however the event itself still was deserialized and loaded into memory. This has two notabledownsides:
- first, that the deserialization was actually performed, so we spent some of out time budget on the deserialization, even though the event does not contribute anything to the persistent actors state.
- second, that we are unable to remove the event class from the system – since the serializer still needs to create the actuall instance of it, as it does not know it will not be used.
The solution to these problems is to use a serializer that is aware of that event being no longer needed, and can notice this before starting to deserialize the object.
This aproach allows us to remove the original class from our classpath, which makes for less "old" classes lying around in the project. This can for example be implemented by using an SerializerWithStringManifest (documented in depth in Serializer with String Manifest). By looking at the string manifest, the serializer can notice that the type is no longer needed, and skip the deserialization all-together:
The serializer detects that the string manifest points to a removed event type and skips attempting to deserialize it:
- case object EventDeserializationSkipped
-
- class RemovedEventsAwareSerializer extends SerializerWithStringManifest {
- val utf8 = Charset.forName("UTF-8")
- override def identifier: Int = 8337
-
- val SkipEventManifestsEvents = Set(
- "docs.persistence.CustomerBlinked" // ...
- )
-
- override def manifest(o: AnyRef): String = o.getClass.getName
-
- override def toBinary(o: AnyRef): Array[Byte] = o match {
- case _ => o.toString.getBytes(utf8) // example serialization
- }
-
- override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef =
- manifest match {
- case m if SkipEventManifestsEvents.contains(m) =>
- EventDeserializationSkipped
-
- case other => new String(bytes, utf8)
- }
- }
The EventAdapter we implemented is aware of EventDeserializationSkipped events (our "Tombstones"), and emits and empty EventSeq whenever such object is encoutered:
- class SkippedEventsAwareAdapter extends EventAdapter {
- override def manifest(event: Any) = ""
- override def toJournal(event: Any) = event
-
- override def fromJournal(event: Any, manifest: String) = event match {
- case EventDeserializationSkipped => EventSeq.empty
- case _ => EventSeq(event)
- }
- }
Detach domain model from data model
Situation: You want to separate the application model (often called the "domain model") completely from the models used to persist the corresponding events (the "data model"). For example because the data representation may change independently of the domain model.
Another situation where this technique may be useful is when your serialization tool of choice requires generated classes to be used for serialization and deserialization of objects, like for example Google Protocol Buffers do, yet you do not want to leak this implementation detail into the domain model itself, which you'd like to model as plain Scala case classes.
Solution: In order to detach the domain model, which is often represented using pure scala (case) classes, from the data model classes which very often may be less user-friendly yet highly optimised for throughput and schema evolution (like the classes generated by protobuf for example), it is possible to use a simple EventAdapter which maps between these types in a 1:1 style as illustrated below:
We will use the following domain and data models to showcase how the separation can be implemented by the adapter:
- /** Domain model - highly optimised for domain language and maybe "fluent" usage */
- object DomainModel {
- final case class Customer(name: String)
- final case class Seat(code: String) {
- def bookFor(customer: Customer): SeatBooked = SeatBooked(code, customer)
- }
-
- final case class SeatBooked(code: String, customer: Customer)
- }
-
- /** Data model - highly optimised for schema evolution and persistence */
- object DataModel {
- final case class SeatBooked(code: String, customerName: String)
- }
The EventAdapter takes care of converting from one model to the other one (in both directions), alowing the models to be completely detached from each other, such that they can be optimised independently as long as the mapping logic is able to convert between them:
- class DetachedModelsAdapter extends EventAdapter {
- override def manifest(event: Any): String = ""
-
- override def toJournal(event: Any): Any = event match {
- case DomainModel.SeatBooked(code, customer) =>
- DataModel.SeatBooked(code, customer.name)
- }
- override def fromJournal(event: Any, manifest: String): EventSeq = event match {
- case DataModel.SeatBooked(code, customerName) =>
- EventSeq(DomainModel.SeatBooked(code, DomainModel.Customer(customerName)))
- }
- }
The same technique could also be used directly in the Serializer if the end result of marshalling is bytes. Then the serializer can simply convert the bytes do the domain object by using the generated protobuf builders.
Store events as human-readable data model
Situation: You want to keep your persisted events in a human-readable format, for example JSON.
Solution: This is a special case of the Detach domain model from data model pattern, and thus requires some co-operation from the Journal implementation to achieve this.
An example of a Journal which may implement this pattern is MongoDB, however other databases such as PostgreSQL and Cassandra could also do it because of their built-in JSON capabilities.
In this aproach, the EventAdapter is used as the marshalling layer: it serializes the events to/from JSON. The journal plugin notices that the incoming event type is JSON (for example by performing a match on the incoming event) and stores the incoming object directly.
- class JsonDataModelAdapter extends EventAdapter {
- override def manifest(event: Any): String = ""
-
- val marshaller = new ExampleJsonMarshaller
-
- override def toJournal(event: Any): JsObject =
- marshaller.toJson(event)
-
- override def fromJournal(event: Any, manifest: String): EventSeq = event match {
- case json: JsObject =>
- EventSeq(marshaller.fromJson(json))
- case _ =>
- throw new IllegalArgumentException(
- "Unable to fromJournal a non-JSON object! Was: " + event.getClass)
- }
- }
Note
This technique only applies if the Akka Persistence plugin you are using provides this capability. Check the documentation of your favourite plugin to see if it supports this style of persistence.
If it doesn't, you may want to skim the list of existing journal plugins, just in case some other plugin for your favourite datastore does provide this capability.
Alternative solution:
In fact, an AsyncWriteJournal implementation could natively decide to not use binary serialization at all, and alwaysserialize the incoming messages as JSON - in which case the toJournal implementation of the EventAdapter would be an identity function, and the fromJournal would need to de-serialize messages from JSON.
Note
If in need of human-readable events on the write-side of your application reconsider whether preparing materialized views using Persistence Query would not be an efficient way to go about this, without compromising the write-side's throughput characteristics.
If indeed you want to use a human-readable representation on the write-side, pick a Persistence plugin that provides that functionality, or – implement one yourself.
Split large event into fine-grained events
Situation: While refactoring your domain events, you find that one of the events has become too large (coarse-grained) and needs to be split up into multiple fine-grained events.
Solution: Let us consider a situation where an event represents "user details changed". After some time we discover that this event is too coarse, and needs to be split into "user name changed" and "user address changed", because somehow users keep changing their usernames a lot and we'd like to keep this as a separate event.
The write side change is very simple, we simply persist UserNameChanged or UserAddressChanged depending on what the user actually intended to change (instead of the composite UserDetailsChanged that we had in version 1 of our model).
During recovery however, we now need to convert the old V1 model into the V2 representation of the change. Depending if the old event contains a name change, we either emit the UserNameChanged or we don't, and the address change is handled similarily:
- trait V1
- trait V2
-
- // V1 event:
- final case class UserDetailsChanged(name: String, address: String) extends V1
-
- // corresponding V2 events:
- final case class UserNameChanged(name: String) extends V2
- final case class UserAddressChanged(address: String) extends V2
-
- // event splitting adapter:
- class UserEventsAdapter extends EventAdapter {
- override def manifest(event: Any): String = ""
-
- override def fromJournal(event: Any, manifest: String): EventSeq = event match {
- case UserDetailsChanged(null, address) => EventSeq(UserAddressChanged(address))
- case UserDetailsChanged(name, null) => EventSeq(UserNameChanged(name))
- case UserDetailsChanged(name, address) =>
- EventSeq(
- UserNameChanged(name),
- UserAddressChanged(address))
- case event: V2 => EventSeq(event)
- }
-
- override def toJournal(event: Any): Any = event
- }
By returning an EventSeq from the event adapter, the recovered event can be converted to multiple events before being delivered to the persistent actor.
Persistence Query
Akka persistence query complements Persistence by providing a universal asynchronous stream based query interface that various journal plugins can implement in order to expose their query capabilities.
The most typical use case of persistence query is implementing the so-called query side (also known as "read side") in the popular CQRS architecture pattern - in which the writing side of the application (e.g. implemented using akka persistence) is completely separated from the "query side". Akka Persistence Query itself is not directly the query side of an application, however it can help to migrate data from the write side to the query side database. In very simple scenarios Persistence Query may be powerful enough to fulfill the query needs of your app, however we highly recommend (in the spirit of CQRS) of splitting up the write/read sides into separate datastores as the need arises.
Warning
This module is marked as “experimental” as of its introduction in Akka 2.4.0. We will continue to improve this API based on our users’ feedback, which implies that while we try to keep incompatible changes to a minimum the binary compatibility guarantee for maintenance releases does not apply to the contents of theakka.persistence.query package.
Dependencies
Akka persistence query is a separate jar file. Make sure that you have the following dependency in your project:
- "com.typesafe.akka" %% "akka-persistence-query-experimental" % "2.4.9"
Design overview
Akka persistence query is purposely designed to be a very loosely specified API. This is in order to keep the provided APIs general enough for each journal implementation to be able to expose its best features, e.g. a SQL journal can use complex SQL queries or if a journal is able to subscribe to a live event stream this should also be possible to expose the same API - a typed stream of events.
Each read journal must explicitly document which types of queries it supports. Refer to your journal's plugins documentation for details on which queries and semantics it supports.
While Akka Persistence Query does not provide actual implementations of ReadJournals, it defines a number of pre-defined query types for the most common query scenarios, that most journals are likely to implement (however they are not required to).
Read Journals
In order to issue queries one has to first obtain an instance of a ReadJournal. Read journals are implemented asCommunity plugins, each targeting a specific datastore (for example Cassandra or JDBC databases). For example, given a library that provides a akka.persistence.query.my-read-journal obtaining the related journal is as simple as:
- // obtain read journal by plugin id
- val readJournal =
- PersistenceQuery(system).readJournalFor[MyScaladslReadJournal](
- "akka.persistence.query.my-read-journal")
-
- // issue query to journal
- val source: Source[EventEnvelope, NotUsed] =
- readJournal.eventsByPersistenceId("user-1337", 0, Long.MaxValue)
-
- // materialize stream, consuming events
- implicit val mat = ActorMaterializer()
- source.runForeach { event => println("Event: " + event) }
Journal implementers are encouraged to put this identifier in a variable known to the user, such that one can access it via readJournalFor[NoopJournal](NoopJournal.identifier), however this is not enforced.
Read journal implementations are available as Community plugins.
Predefined queries
Akka persistence query comes with a number of query interfaces built in and suggests Journal implementors to implement them according to the semantics described below. It is important to notice that while these query types are very common a journal is not obliged to implement all of them - for example because in a given journal such query would be significantly inefficient.
Note
Refer to the documentation of the ReadJournal plugin you are using for a specific list of supported query types. For example, Journal plugins should document their stream completion strategies.
The predefined queries are:
AllPersistenceIdsQuery and CurrentPersistenceIdsQuery
allPersistenceIds which is designed to allow users to subscribe to a stream of all persistent ids in the system. By default this stream should be assumed to be a "live" stream, which means that the journal should keep emitting new persistence ids as they come into the system:
- readJournal.allPersistenceIds()
If your usage does not require a live stream, you can use the currentPersistenceIds query:
- readJournal.currentPersistenceIds()
EventsByPersistenceIdQuery and CurrentEventsByPersistenceIdQuery
eventsByPersistenceId is a query equivalent to replaying a PersistentActor, however, since it is a stream it is possible to keep it alive and watch for additional incoming events persisted by the persistent actor identified by the given persistenceId.
- readJournal.eventsByPersistenceId("user-us-1337")
Most journals will have to revert to polling in order to achieve this, which can typically be configured with a refresh-interval configuration property.
If your usage does not require a live stream, you can use the currentEventsByPersistenceId query.
EventsByTag and CurrentEventsByTag
eventsByTag allows querying events regardless of which persistenceId they are associated with. This query is hard to implement in some journals or may need some additional preparation of the used data store to be executed efficiently. The goal of this query is to allow querying for all events which are "tagged" with a specific tag. That includes the use case to query all domain events of an Aggregate Root type. Please refer to your read journal plugin's documentation to find out if and how it is supported.
Some journals may support tagging of events via an Event Adapters that wraps the events in aakka.persistence.journal.Tagged with the given tags. The journal may support other ways of doing tagging - again, how exactly this is implemented depends on the used journal. Here is an example of such a tagging event adapter:
- import akka.persistence.journal.WriteEventAdapter
- import akka.persistence.journal.Tagged
-
- class MyTaggingEventAdapter extends WriteEventAdapter {
- val colors = Set("green", "black", "blue")
- override def toJournal(event: Any): Any = event match {
- case s: String =>
- var tags = colors.foldLeft(Set.empty[String]) { (acc, c) =>
- if (s.contains(c)) acc + c else acc
- }
- if (tags.isEmpty) event
- else Tagged(event, tags)
- case _ => event
- }
-
- override def manifest(event: Any): String = ""
- }
Note
A very important thing to keep in mind when using queries spanning multiple persistenceIds, such asEventsByTag is that the order of events at which the events appear in the stream rarely is guaranteed (or stable between materializations).
Journals may choose to opt for strict ordering of the events, and should then document explicitly what kind of ordering guarantee they provide - for example "ordered by timestamp ascending, independently of persistenceId" is easy to achieve on relational databases, yet may be hard to implement efficiently on plain key-value datastores.
In the example below we query all events which have been tagged (we assume this was performed by the write-side using an EventAdapter, or that the journal is smart enough that it can figure out what we mean by this tag - for example if the journal stored the events as json it may try to find those with the field tag set to this value etc.).
- // assuming journal is able to work with numeric offsets we can:
-
- val blueThings: Source[EventEnvelope, NotUsed] =
- readJournal.eventsByTag("blue")
-
- // find top 10 blue things:
- val top10BlueThings: Future[Vector[Any]] =
- blueThings
- .map(_.event)
- .take(10) // cancels the query stream after pulling 10 elements
- .runFold(Vector.empty[Any])(_ :+ _)
-
- // start another query, from the known offset
- val furtherBlueThings = readJournal.eventsByTag("blue", offset = 10)
As you can see, we can use all the usual stream combinators available from Akka Streams on the resulting query stream, including for example taking the first 10 and cancelling the stream. It is worth pointing out that the built-inEventsByTag query has an optionally supported offset parameter (of type Long) which the journals can use to implement resumable-streams. For example a journal may be able to use a WHERE clause to begin the read starting from a specific row, or in a datastore that is able to order events by insertion time it could treat the Long as a timestamp and select only older events.
If your usage does not require a live stream, you can use the currentEventsByTag query.
Materialized values of queries
Journals are able to provide additional information related to a query by exposing materialized values, which are a feature of Akka Streams that allows to expose additional values at stream materialization time.
More advanced query journals may use this technique to expose information about the character of the materialized stream, for example if it's finite or infinite, strictly ordered or not ordered at all. The materialized value type is defined as the second type parameter of the returned Source, which allows journals to provide users with their specialised query object, as demonstrated in the sample below:
- final case class RichEvent(tags: Set[String], payload: Any)
-
- // a plugin can provide:
- case class QueryMetadata(deterministicOrder: Boolean, infinite: Boolean)
- def byTagsWithMeta(tags: Set[String]): Source[RichEvent, QueryMetadata] = {
- val query: Source[RichEvent, QueryMetadata] =
- readJournal.byTagsWithMeta(Set("red", "blue"))
-
- query
- .mapMaterializedValue { meta =>
- println(s"The query is: " +
- s"ordered deterministically: ${meta.deterministicOrder}, " +
- s"infinite: ${meta.infinite}")
- }
- .map { event => println(s"Event payload: ${event.payload}") }
- .runWith(Sink.ignore)
Query plugins
Query plugins are various (mostly community driven) ReadJournal implementations for all kinds of available datastores. The complete list of available plugins is maintained on the Akka Persistence Query Community Plugins page.
The plugin for LevelDB is described in Persistence Query for LevelDB.
This section aims to provide tips and guide plugin developers through implementing a custom query plugin. Most users will not need to implement journals themselves, except if targeting a not yet supported datastore.
Note
Since different data stores provide different query capabilities journal plugins must extensively document their exposed semantics as well as handled query scenarios.
ReadJournal plugin API
A read journal plugin must implement akka.persistence.query.ReadJournalProvider which creates instances ofakka.persistence.query.scaladsl.ReadJournal and akka.persistence.query.javaadsl.ReadJournal. The plugin must implement both the scaladsl and the javadsl traits because the akka.stream.scaladsl.Sourceand akka.stream.javadsl.Source are different types and even though those types can easily be converted to each other it is most convenient for the end user to get access to the Java or Scala directly. As illustrated below one of the implementations can delegate to the other.
Below is a simple journal implementation:
- class MyReadJournalProvider(system: ExtendedActorSystem, config: Config)
- extends ReadJournalProvider {
-
- override val scaladslReadJournal: MyScaladslReadJournal =
- new MyScaladslReadJournal(system, config)
-
- override val javadslReadJournal: MyJavadslReadJournal =
- new MyJavadslReadJournal(scaladslReadJournal)
- }
-
- class MyScaladslReadJournal(system: ExtendedActorSystem, config: Config)
- extends akka.persistence.query.scaladsl.ReadJournal
- with akka.persistence.query.scaladsl.EventsByTagQuery
- with akka.persistence.query.scaladsl.EventsByPersistenceIdQuery
- with akka.persistence.query.scaladsl.AllPersistenceIdsQuery
- with akka.persistence.query.scaladsl.CurrentPersistenceIdsQuery {
-
- private val refreshInterval: FiniteDuration =
- config.getDuration("refresh-interval", MILLISECONDS).millis
-
- override def eventsByTag(
- tag: String, offset: Long = 0L): Source[EventEnvelope, NotUsed] = {
- val props = MyEventsByTagPublisher.props(tag, offset, refreshInterval)
- Source.actorPublisher[EventEnvelope](props)
- .mapMaterializedValue(_ => NotUsed)
- }
-
- override def eventsByPersistenceId(
- persistenceId: String, fromSequenceNr: Long = 0L,
- toSequenceNr: Long = Long.MaxValue): Source[EventEnvelope, NotUsed] = {
- // implement in a similar way as eventsByTag
- ???
- }
-
- override def allPersistenceIds(): Source[String, NotUsed] = {
- // implement in a similar way as eventsByTag
- ???
- }
-
- override def currentPersistenceIds(): Source[String, NotUsed] = {
- // implement in a similar way as eventsByTag
- ???
- }
-
- // possibility to add more plugin specific queries
-
- def byTagsWithMeta(tags: Set[String]): Source[RichEvent, QueryMetadata] = {
- // implement in a similar way as eventsByTag
- ???
- }
-
- }
-
- class MyJavadslReadJournal(scaladslReadJournal: MyScaladslReadJournal)
- extends akka.persistence.query.javadsl.ReadJournal
- with akka.persistence.query.javadsl.EventsByTagQuery
- with akka.persistence.query.javadsl.EventsByPersistenceIdQuery
- with akka.persistence.query.javadsl.AllPersistenceIdsQuery
- with akka.persistence.query.javadsl.CurrentPersistenceIdsQuery {
-
- override def eventsByTag(
- tag: String, offset: Long = 0L): javadsl.Source[EventEnvelope, NotUsed] =
- scaladslReadJournal.eventsByTag(tag, offset).asJava
-
- override def eventsByPersistenceId(
- persistenceId: String, fromSequenceNr: Long = 0L,
- toSequenceNr: Long = Long.MaxValue): javadsl.Source[EventEnvelope, NotUsed] =
- scaladslReadJournal.eventsByPersistenceId(
- persistenceId, fromSequenceNr, toSequenceNr).asJava
-
- override def allPersistenceIds(): javadsl.Source[String, NotUsed] =
- scaladslReadJournal.allPersistenceIds().asJava
-
- override def currentPersistenceIds(): javadsl.Source[String, NotUsed] =
- scaladslReadJournal.currentPersistenceIds().asJava
-
- // possibility to add more plugin specific queries
-
- def byTagsWithMeta(
- tags: java.util.Set[String]): javadsl.Source[RichEvent, QueryMetadata] = {
- import scala.collection.JavaConverters._
- scaladslReadJournal.byTagsWithMeta(tags.asScala.toSet).asJava
- }
- }
And the eventsByTag could be backed by such an Actor for example:
- class MyEventsByTagPublisher(tag: String, offset: Long, refreshInterval: FiniteDuration)
- extends ActorPublisher[EventEnvelope] {
-
- private case object Continue
-
- private val connection: java.sql.Connection = ???
-
- private val Limit = 1000
- private var currentOffset = offset
- var buf = Vector.empty[EventEnvelope]
-
- import context.dispatcher
- val continueTask = context.system.scheduler.schedule(
- refreshInterval, refreshInterval, self, Continue)
-
- override def postStop(): Unit = {
- continueTask.cancel()
- }
-
- def receive = {
- case _: Request | Continue =>
- query()
- deliverBuf()
-
- case Cancel =>
- context.stop(self)
- }
-
- object Select {
- private def statement() = connection.prepareStatement(
- """
- SELECT id, persistent_repr FROM journal
- WHERE tag = ? AND id >= ?
- ORDER BY id LIMIT ?
- """)
-
- def run(tag: String, from: Long, limit: Int): Vector[(Long, Array[Byte])] = {
- val s = statement()
- try {
- s.setString(1, tag)
- s.setLong(2, from)
- s.setLong(3, limit)
- val rs = s.executeQuery()
-
- val b = Vector.newBuilder[(Long, Array[Byte])]
- while (rs.next())
- b += (rs.getLong(1) -> rs.getBytes(2))
- b.result()
- } finally s.close()
- }
- }
-
- def query(): Unit =
- if (buf.isEmpty) {
- try {
- val result = Select.run(tag, currentOffset, Limit)
- currentOffset = if (result.nonEmpty) result.last._1 else currentOffset
- val serialization = SerializationExtension(context.system)
-
- buf = result.map {
- case (id, bytes) =>
- val p = serialization.deserialize(bytes, classOf[PersistentRepr]).get
- EventEnvelope(offset = id, p.persistenceId, p.sequenceNr, p.payload)
- }
- } catch {
- case e: Exception =>
- onErrorThenStop(e)
- }
- }
-
- final def deliverBuf(): Unit =
- if (totalDemand > 0 && buf.nonEmpty) {
- if (totalDemand <= Int.MaxValue) {
- val (use, keep) = buf.splitAt(totalDemand.toInt)
- buf = keep
- use foreach onNext
- } else {
- buf foreach onNext
- buf = Vector.empty
- }
- }
- }
If the underlying datastore only supports queries that are completed when they reach the end of the "result set", the journal has to submit new queries after a while in order to support "infinite" event streams that include events stored after the initial query has completed. It is recommended that the plugin use a configuration property named refresh-interval for defining such a refresh interval.
Plugin TCK
TODO, not available yet.
Persistence Query for LevelDB
This is documentation for the LevelDB implementation of the Persistence Query API. Note that implementations for other journals may have different semantics.
Warning
This module is marked as “experimental” as of its introduction in Akka 2.4.0. We will continue to improve this API based on our users’ feedback, which implies that while we try to keep incompatible changes to a minimum the binary compatibility guarantee for maintenance releases does not apply to the contents of theakka.persistence.query package.
Dependencies
Akka persistence LevelDB query implementation is bundled in the akka-persistence-query-experimental artifact. Make sure that you have the following dependency in your project:
- "com.typesafe.akka" %% "akka-persistence-query-experimental" % "2.4.9"
How to get the ReadJournal
The ReadJournal is retrieved via the akka.persistence.query.PersistenceQuery extension:
- import akka.persistence.query.PersistenceQuery
- import akka.persistence.query.journal.leveldb.scaladsl.LeveldbReadJournal
-
- val queries = PersistenceQuery(system).readJournalFor[LeveldbReadJournal](
- LeveldbReadJournal.Identifier)
Supported Queries
EventsByPersistenceIdQuery and CurrentEventsByPersistenceIdQuery
eventsByPersistenceId is used for retrieving events for a specific PersistentActor identified bypersistenceId.
- implicit val mat = ActorMaterializer()(system)
- val queries = PersistenceQuery(system).readJournalFor[LeveldbReadJournal](
- LeveldbReadJournal.Identifier)
-
- val src: Source[EventEnvelope, NotUsed] =
- queries.eventsByPersistenceId("some-persistence-id", 0L, Long.MaxValue)
-
- val events: Source[Any, NotUsed] = src.map(_.event)
You can retrieve a subset of all events by specifying fromSequenceNr and toSequenceNr or use 0L andLong.MaxValue respectively to retrieve all events. Note that the corresponding sequence number of each event is provided in the EventEnvelope, which makes it possible to resume the stream at a later point from a given sequence number.
The returned event stream is ordered by sequence number, i.e. the same order as the PersistentActor persisted the events. The same prefix of stream elements (in same order) are returned for multiple executions of the query, except for when events have been deleted.
The stream is not completed when it reaches the end of the currently stored events, but it continues to push new events when new events are persisted. Corresponding query that is completed when it reaches the end of the currently stored events is provided by currentEventsByPersistenceId.
The LevelDB write journal is notifying the query side as soon as events are persisted, but for efficiency reasons the query side retrieves the events in batches that sometimes can be delayed up to the configured refresh-interval or givenRefreshInterval hint.
The stream is completed with failure if there is a failure in executing the query in the backend journal.
AllPersistenceIdsQuery and CurrentPersistenceIdsQuery
allPersistenceIds is used for retrieving all persistenceIds of all persistent actors.
- implicit val mat = ActorMaterializer()(system)
- val queries = PersistenceQuery(system).readJournalFor[LeveldbReadJournal](
- LeveldbReadJournal.Identifier)
-
- val src: Source[String, NotUsed] = queries.allPersistenceIds()
The returned event stream is unordered and you can expect different order for multiple executions of the query.
The stream is not completed when it reaches the end of the currently used persistenceIds, but it continues to push newpersistenceIds when new persistent actors are created. Corresponding query that is completed when it reaches the end of the currently used persistenceIds is provided by currentPersistenceIds.
The LevelDB write journal is notifying the query side as soon as new persistenceIds are created and there is no periodic polling or batching involved in this query.
The stream is completed with failure if there is a failure in executing the query in the backend journal.
EventsByTag and CurrentEventsByTag
eventsByTag is used for retrieving events that were marked with a given tag, e.g. all domain events of an Aggregate Root type.
- implicit val mat = ActorMaterializer()(system)
- val queries = PersistenceQuery(system).readJournalFor[LeveldbReadJournal](
- LeveldbReadJournal.Identifier)
-
- val src: Source[EventEnvelope, NotUsed] =
- queries.eventsByTag(tag = "green", offset = 0L)
To tag events you create an Event Adapters that wraps the events in a akka.persistence.journal.Tagged with the given tags.
- import akka.persistence.journal.WriteEventAdapter
- import akka.persistence.journal.Tagged
-
- class MyTaggingEventAdapter extends WriteEventAdapter {
- val colors = Set("green", "black", "blue")
- override def toJournal(event: Any): Any = event match {
- case s: String =>
- var tags = colors.foldLeft(Set.empty[String]) { (acc, c) =>
- if (s.contains(c)) acc + c else acc
- }
- if (tags.isEmpty) event
- else Tagged(event, tags)
- case _ => event
- }
-
- override def manifest(event: Any): String = ""
- }
You can retrieve a subset of all events by specifying offset, or use 0L to retrieve all events with a given tag. Theoffset corresponds to an ordered sequence number for the specific tag. Note that the corresponding offset of each event is provided in the EventEnvelope, which makes it possible to resume the stream at a later point from a given offset.
In addition to the offset the EventEnvelope also provides persistenceId and sequenceNr for each event. ThesequenceNr is the sequence number for the persistent actor with the persistenceId that persisted the event. ThepersistenceId + sequenceNr is an unique identifier for the event.
The returned event stream is ordered by the offset (tag sequence number), which corresponds to the same order as the write journal stored the events. The same stream elements (in same order) are returned for multiple executions of the query. Deleted events are not deleted from the tagged event stream.
Note
Events deleted using deleteMessages(toSequenceNr) are not deleted from the "tagged stream".
The stream is not completed when it reaches the end of the currently stored events, but it continues to push new events when new events are persisted. Corresponding query that is completed when it reaches the end of the currently stored events is provided by currentEventsByTag.
The LevelDB write journal is notifying the query side as soon as tagged events are persisted, but for efficiency reasons the query side retrieves the events in batches that sometimes can be delayed up to the configured refresh-intervalor given RefreshInterval hint.
The stream is completed with failure if there is a failure in executing the query in the backend journal.
Configuration
Configuration settings can be defined in the configuration section with the absolute path corresponding to the identifier, which is "akka.persistence.query.journal.leveldb" for the default LeveldbReadJournal.Identifier.
It can be configured with the following properties:
- # Configuration for the LeveldbReadJournal
- akka.persistence.query.journal.leveldb {
- # Implementation class of the LevelDB ReadJournalProvider
- class = "akka.persistence.query.journal.leveldb.LeveldbReadJournalProvider"
-
- # Absolute path to the write journal plugin configuration entry that this
- # query journal will connect to. That must be a LeveldbJournal or SharedLeveldbJournal.
- # If undefined (or "") it will connect to the default journal as specified by the
- # akka.persistence.journal.plugin property.
- write-plugin = ""
-
- # The LevelDB write journal is notifying the query side as soon as things
- # are persisted, but for efficiency reasons the query side retrieves the events
- # in batches that sometimes can be delayed up to the configured `refresh-interval`.
- refresh-interval = 3s
-
- # How many events to fetch in one query (replay) and keep buffered until they
- # are delivered downstreams.
- max-buffer-size = 100
- }
Testing Actor Systems
As with any piece of software, automated tests are a very important part of the development cycle. The actor model presents a different view on how units of code are delimited and how they interact, which has an influence on how to perform tests.
Akka comes with a dedicated module akka-testkit for supporting tests at different levels, which fall into two clearly distinct categories:
- Testing isolated pieces of code without involving the actor model, meaning without multiple threads; this implies completely deterministic behavior concerning the ordering of events and no concurrency concerns and will be calledUnit Testing in the following.
- Testing (multiple) encapsulated actors including multi-threaded scheduling; this implies non-deterministic order of events but shielding from concurrency concerns by the actor model and will be called Integration Testing in the following.
There are of course variations on the granularity of tests in both categories, where unit testing reaches down to white-box tests and integration testing can encompass functional tests of complete actor networks. The important distinction lies in whether concurrency concerns are part of the test or not. The tools offered are described in detail in the following sections.
Note
Be sure to add the module akka-testkit to your dependencies.
Synchronous Unit Testing with TestActorRef
Testing the business logic inside Actor classes can be divided into two parts: first, each atomic operation must work in isolation, then sequences of incoming events must be processed correctly, even in the presence of some possible variability in the ordering of events. The former is the primary use case for single-threaded unit testing, while the latter can only be verified in integration tests.
Normally, the ActorRef shields the underlying Actor instance from the outside, the only communications channel is the actor's mailbox. This restriction is an impediment to unit testing, which led to the inception of the TestActorRef. This special type of reference is designed specifically for test purposes and allows access to the actor in two ways: either by obtaining a reference to the underlying actor instance, or by invoking or querying the actor's behaviour (receive). Each one warrants its own section below.
Note
It is highly recommended to stick to traditional behavioural testing (using messaging to ask the Actor to reply with the state you want to run assertions against), instead of using TestActorRef whenever possible.
Warning
Due to the synchronous nature of TestActorRef it will not work with some support traits that Akka provides as they require asynchronous behaviours to function properly. Examples of traits that do not mix well with test actor refs are PersistentActor and AtLeastOnceDelivery provided by Akka Persistence.
Obtaining a Reference to an Actor
Having access to the actual Actor object allows application of all traditional unit testing techniques on the contained methods. Obtaining a reference is done like this:
- import akka.testkit.TestActorRef
-
- val actorRef = TestActorRef[MyActor]
- val actor = actorRef.underlyingActor
Since TestActorRef is generic in the actor type it returns the underlying actor with its proper static type. From this point on you may bring any unit testing tool to bear on your actor as usual.
Testing Finite State Machines
If your actor under test is a FSM, you may use the special TestFSMRef which offers all features of a normalTestActorRef and in addition allows access to the internal state:
- import akka.testkit.TestFSMRef
- import akka.actor.FSM
- import scala.concurrent.duration._
-
- val fsm = TestFSMRef(new TestFsmActor)
-
- val mustBeTypedProperly: TestActorRef[TestFsmActor] = fsm
-
- assert(fsm.stateName == 1)
- assert(fsm.stateData == "")
- fsm ! "go" // being a TestActorRef, this runs also on the CallingThreadDispatcher
- assert(fsm.stateName == 2)
- assert(fsm.stateData == "go")
-
- fsm.setState(stateName = 1)
- assert(fsm.stateName == 1)
-
- assert(fsm.isTimerActive("test") == false)
- fsm.setTimer("test", 12, 10 millis, true)
- assert(fsm.isTimerActive("test") == true)
- fsm.cancelTimer("test")
- assert(fsm.isTimerActive("test") == false)
Due to a limitation in Scala’s type inference, there is only the factory method shown above, so you will probably write code like TestFSMRef(new MyFSM) instead of the hypothetical ActorRef-inspired TestFSMRef[MyFSM]. All methods shown above directly access the FSM state without any synchronization; this is perfectly alright if theCallingThreadDispatcher is used and no other threads are involved, but it may lead to surprises if you were to actually exercise timer events, because those are executed on the Scheduler thread.
Testing the Actor's Behavior
When the dispatcher invokes the processing behavior of an actor on a message, it actually calls apply on the current behavior registered for the actor. This starts out with the return value of the declared receive method, but it may also be changed using become and unbecome in response to external messages. All of this contributes to the overall actor behavior and it does not lend itself to easy testing on the Actor itself. Therefore the TestActorRef offers a different mode of operation to complement the Actor testing: it supports all operations also valid on normal ActorRef. Messages sent to the actor are processed synchronously on the current thread and answers may be sent back as usual. This trick is made possible by the CallingThreadDispatcher described below (see CallingThreadDispatcher); this dispatcher is set implicitly for any actor instantiated into a TestActorRef.
- import akka.testkit.TestActorRef
- import scala.concurrent.duration._
- import scala.concurrent.Await
- import akka.pattern.ask
-
- val actorRef = TestActorRef(new MyActor)
- // hypothetical message stimulating a '42' answer
- val future = actorRef ? Say42
- val Success(result: Int) = future.value.get
- result should be(42)
As the TestActorRef is a subclass of LocalActorRef with a few special extras, also aspects like supervision and restarting work properly, but beware that execution is only strictly synchronous as long as all actors involved use theCallingThreadDispatcher. As soon as you add elements which include more sophisticated scheduling you leave the realm of unit testing as you then need to think about asynchronicity again (in most cases the problem will be to wait until the desired effect had a chance to happen).
One more special aspect which is overridden for single-threaded tests is the receiveTimeout, as including that would entail asynchronous queuing of ReceiveTimeout messages, violating the synchronous contract.
Note
To summarize: TestActorRef overwrites two fields: it sets the dispatcher toCallingThreadDispatcher.global and it sets the receiveTimeout to None.
The Way In-Between: Expecting Exceptions
If you want to test the actor behavior, including hotswapping, but without involving a dispatcher and without having theTestActorRef swallow any thrown exceptions, then there is another mode available for you: just use the receivemethod on TestActorRef, which will be forwarded to the underlying actor:
- import akka.testkit.TestActorRef
-
- val actorRef = TestActorRef(new Actor {
- def receive = {
- case "hello" => throw new IllegalArgumentException("boom")
- }
- })
- intercept[IllegalArgumentException] { actorRef.receive("hello") }
Use Cases
You may of course mix and match both modi operandi of TestActorRef as suits your test needs:
- one common use case is setting up the actor into a specific internal state before sending the test message
- another is to verify correct internal state transitions after having sent the test message
Feel free to experiment with the possibilities, and if you find useful patterns, don't hesitate to let the Akka forums know about them! Who knows, common operations might even be worked into nice DSLs.
Asynchronous Integration Testing with TestKit
When you are reasonably sure that your actor's business logic is correct, the next step is verifying that it works correctly within its intended environment (if the individual actors are simple enough, possibly because they use the FSMmodule, this might also be the first step). The definition of the environment depends of course very much on the problem at hand and the level at which you intend to test, ranging for functional/integration tests to full system tests. The minimal setup consists of the test procedure, which provides the desired stimuli, the actor under test, and an actor receiving replies. Bigger systems replace the actor under test with a network of actors, apply stimuli at varying injection points and arrange results to be sent from different emission points, but the basic principle stays the same in that a single procedure drives the test.
The TestKit class contains a collection of tools which makes this common task easy.
- import akka.actor.ActorSystem
- import akka.actor.Actor
- import akka.actor.Props
- import akka.testkit.{ TestActors, TestKit, ImplicitSender }
- import org.scalatest.WordSpecLike
- import org.scalatest.Matchers
- import org.scalatest.BeforeAndAfterAll
-
- class MySpec() extends TestKit(ActorSystem("MySpec")) with ImplicitSender
- with WordSpecLike with Matchers with BeforeAndAfterAll {
-
- override def afterAll {
- TestKit.shutdownActorSystem(system)
- }
-
- "An Echo actor" must {
-
- "send back messages unchanged" in {
- val echo = system.actorOf(TestActors.echoActorProps)
- echo ! "hello world"
- expectMsg("hello world")
- }
-
- }
- }
The TestKit contains an actor named testActor which is the entry point for messages to be examined with the various expectMsg... assertions detailed below. When mixing in the trait ImplicitSender this test actor is implicitly used as sender reference when dispatching messages from the test procedure. The testActor may also be passed to other actors as usual, usually subscribing it as notification listener. There is a whole set of examination methods, e.g. receiving all consecutive messages matching certain criteria, receiving a whole sequence of fixed messages or classes, receiving nothing for some time, etc.
The ActorSystem passed in to the constructor of TestKit is accessible via the system member. Remember to shut down the actor system after the test is finished (also in case of failure) so that all actors—including the test actor—are stopped.
Built-In Assertions
The above mentioned expectMsg is not the only method for formulating assertions concerning received messages. Here is the full list:
expectMsg[T](d: Duration, msg: T): T
The given message object must be received within the specified time; the object will be returned.
expectMsgPF[T](d: Duration)(pf: PartialFunction[Any, T]): T
Within the given time period, a message must be received and the given partial function must be defined for that message; the result from applying the partial function to the received message is returned. The duration may be left unspecified (empty parentheses are required in this case) to use the deadline from the innermost enclosing within block instead.
expectMsgClass[T](d: Duration, c: Class[T]): T
An object which is an instance of the given Class must be received within the allotted time frame; the object will be returned. Note that this does a conformance check; if you need the class to be equal, have a look at expectMsgAllClassOf with a single given class argument.
expectMsgType[T: Manifest](d: Duration)
An object which is an instance of the given type (after erasure) must be received within the allotted time frame; the object will be returned. This method is approximately equivalent toexpectMsgClass(implicitly[ClassTag[T]].runtimeClass).
expectMsgAnyOf[T](d: Duration, obj: T*): T
An object must be received within the given time, and it must be equal ( compared with ==) to at least one of the passed reference objects; the received object will be returned.
expectMsgAnyClassOf[T](d: Duration, obj: Class[_ <: T]*): T
An object must be received within the given time, and it must be an instance of at least one of the supplied Class objects; the received object will be returned.
expectMsgAllOf[T](d: Duration, obj: T*): Seq[T]
A number of objects matching the size of the supplied object array must be received within the given time, and for each of the given objects there must exist at least one among the received ones which equals (compared with ==) it. The full sequence of received objects is returned.
expectMsgAllClassOf[T](d: Duration, c: Class[_ <: T]*): Seq[T]
A number of objects matching the size of the supplied Class array must be received within the given time, and for each of the given classes there must exist at least one among the received objects whose class equals (compared with ==) it (this is not a conformance check). The full sequence of received objects is returned.
expectMsgAllConformingOf[T](d: Duration, c: Class[_ <: T]*): Seq[T]
A number of objects matching the size of the supplied Class array must be received within the given time, and for each of the given classes there must exist at least one among the received objects which is an instance of this class. The full sequence of received objects is returned.
expectNoMsg(d: Duration)
No message must be received within the given time. This also fails if a message has been received before calling this method which has not been removed from the queue using one of the other methods.
receiveN(n: Int, d: Duration): Seq[AnyRef]
n messages must be received within the given time; the received messages are returned.
fishForMessage(max: Duration, hint: String)(pf: PartialFunction[Any, Boolean]): Any
Keep receiving messages as long as the time is not used up and the partial function matches and returns false. Returns the message received for which it returned true or throws an exception, which will include the provided hint for easier debugging.
In addition to message reception assertions there are also methods which help with message flows:
receiveOne(d: Duration): AnyRef
Tries to receive one message for at most the given time interval and returns null in case of failure. If the given Duration is zero, the call is non-blocking (polling mode).
receiveWhile[T](max: Duration, idle: Duration, messages: Int)(pf: PartialFunction[Any, T]): Seq[T]
Collect messages as long as
- they are matching the given partial function
- the given time interval is not used up
- the next message is received within the idle timeout
- the number of messages has not yet reached the maximum
All collected messages are returned. The maximum duration defaults to the time remaining in the innermost enclosing within block and the idle duration defaults to infinity (thereby disabling the idle timeout feature). The number of expected messages defaults to Int.MaxValue, which effectively disables this limit.
awaitCond(p: => Boolean, max: Duration, interval: Duration)
Poll the given condition every interval until it returns true or the max duration is used up. The interval defaults to 100 ms and the maximum defaults to the time remaining in the innermost enclosing within block.
awaitAssert(a: => Any, max: Duration, interval: Duration)
Poll the given assert function every interval until it does not throw an exception or the maxduration is used up. If the timeout expires the last exception is thrown. The interval defaults to 100 ms and the maximum defaults to the time remaining in the innermost enclosing within block.The interval defaults to 100 ms and the maximum defaults to the time remaining in the innermost enclosing withinblock.
ignoreMsg(pf: PartialFunction[AnyRef, Boolean])
ignoreNoMsg
The internal testActor contains a partial function for ignoring messages: it will only enqueue messages which do not match the function or for which the function returns false. This function can be set and reset using the methods given above; each invocation replaces the previous function, they are not composed.
This feature is useful e.g. when testing a logging system, where you want to ignore regular messages and are only interested in your specific ones.
Expecting Log Messages
Since an integration test does not allow to the internal processing of the participating actors, verifying expected exceptions cannot be done directly. Instead, use the logging system for this purpose: replacing the normal event handler with the TestEventListener and using an EventFilter allows assertions on log messages, including those which are generated by exceptions:
- import akka.testkit.EventFilter
- import com.typesafe.config.ConfigFactory
-
- implicit val system = ActorSystem("testsystem", ConfigFactory.parseString("""
- akka.loggers = ["akka.testkit.TestEventListener"]
- """))
- try {
- val actor = system.actorOf(Props.empty)
- EventFilter[ActorKilledException](occurrences = 1) intercept {
- actor ! Kill
- }
- } finally {
- shutdown(system)
- }
If a number of occurrences is specific—as demonstrated above—then intercept will block until that number of matching messages have been received or the timeout configured in akka.test.filter-leeway is used up (time starts counting after the passed-in block of code returns). In case of a timeout the test fails.
Note
Be sure to exchange the default logger with the TestEventListener in your application.conf to enable this function:
- akka.loggers = [akka.testkit.TestEventListener]
Timing Assertions
Another important part of functional testing concerns timing: certain events must not happen immediately (like a timer), others need to happen before a deadline. Therefore, all examination methods accept an upper time limit within the positive or negative result must be obtained. Lower time limits need to be checked external to the examination, which is facilitated by a new construct for managing time constraints:
- within([min, ]max) {
- ...
- }
The block given to within must complete after a Duration which is between min and max, where the former defaults to zero. The deadline calculated by adding the max parameter to the block's start time is implicitly available within the block to all examination methods, if you do not specify it, it is inherited from the innermost enclosingwithin block.
It should be noted that if the last message-receiving assertion of the block is expectNoMsg or receiveWhile, the final check of the within is skipped in order to avoid false positives due to wake-up latencies. This means that while individual contained assertions still use the maximum time bound, the overall block may take arbitrarily longer in this case.
- import akka.actor.Props
- import scala.concurrent.duration._
-
- val worker = system.actorOf(Props[Worker])
- within(200 millis) {
- worker ! "some work"
- expectMsg("some result")
- expectNoMsg // will block for the rest of the 200ms
- Thread.sleep(300) // will NOT make this block fail
- }
Note
All times are measured using System.nanoTime, meaning that they describe wall time, not CPU time.
Ray Roestenburg has written a great article on using the TestKit: http://roestenburg.agilesquad.com/2011/02/unit-testing-akka-actors-with-testkit_12.html. His full example is also available here.
Accounting for Slow Test Systems
The tight timeouts you use during testing on your lightning-fast notebook will invariably lead to spurious test failures on the heavily loaded Jenkins server (or similar). To account for this situation, all maximum durations are internally scaled by a factor taken from the Configuration, akka.test.timefactor, which defaults to 1.
You can scale other durations with the same factor by using the implicit conversion in akka.testkit package object to add dilated function to Duration.
- import scala.concurrent.duration._
- import akka.testkit._
- 10.milliseconds.dilated
Resolving Conflicts with Implicit ActorRef
If you want the sender of messages inside your TestKit-based tests to be the testActor simply mix inImplicitSender into your test.
- class MySpec() extends TestKit(ActorSystem("MySpec")) with ImplicitSender
- with WordSpecLike with Matchers with BeforeAndAfterAll {
Using Multiple Probe Actors
When the actors under test are supposed to send various messages to different destinations, it may be difficult distinguishing the message streams arriving at the testActor when using the TestKit as a mixin. Another approach is to use it for creation of simple probe actors to be inserted in the message flows. To make this more powerful and convenient, there is a concrete implementation called TestProbe. The functionality is best explained using a small example:
- import scala.concurrent.duration._
- import scala.concurrent.Future
- import akka.actor._
- import akka.testkit.TestProbe
- class MyDoubleEcho extends Actor {
- var dest1: ActorRef = _
- var dest2: ActorRef = _
- def receive = {
- case (d1: ActorRef, d2: ActorRef) =>
- dest1 = d1
- dest2 = d2
- case x =>
- dest1 ! x
- dest2 ! x
- }
- }
- val probe1 = TestProbe()
- val probe2 = TestProbe()
- val actor = system.actorOf(Props[MyDoubleEcho])
- actor ! ((probe1.ref, probe2.ref))
- actor ! "hello"
- probe1.expectMsg(500 millis, "hello")
- probe2.expectMsg(500 millis, "hello")
Here a the system under test is simulated by MyDoubleEcho, which is supposed to mirror its input to two outputs. Attaching two test probes enables verification of the (simplistic) behavior. Another example would be two actors A and B which collaborate by A sending messages to B. In order to verify this message flow, a TestProbe could be inserted as target of A, using the forwarding capabilities or auto-pilot described below to include a real B in the test setup.
If you have many test probes, you can name them to get meaningful actor names in test logs and assertions:
- val worker = TestProbe("worker")
- val aggregator = TestProbe("aggregator")
-
- worker.ref.path.name should startWith("worker")
- aggregator.ref.path.name should startWith("aggregator")
Probes may also be equipped with custom assertions to make your test code even more concise and clear:
- final case class Update(id: Int, value: String)
-
- val probe = new TestProbe(system) {
- def expectUpdate(x: Int) = {
- expectMsgPF() {
- case Update(id, _) if id == x => true
- }
- sender() ! "ACK"
- }
- }
You have complete flexibility here in mixing and matching the TestKit facilities with your own checks and choosing an intuitive name for it. In real life your code will probably be a bit more complicated than the example given above; just use the power!
Warning
Any message send from a TestProbe to another actor which runs on the CallingThreadDispatcher runs the risk of dead-lock, if that other actor might also send to this probe. The implementation of TestProbe.watch andTestProbe.unwatch will also send a message to the watchee, which means that it is dangerous to try watching e.g. TestActorRef from a TestProbe.
Watching Other Actors from Probes
A TestProbe can register itself for DeathWatch of any other actor:
- val probe = TestProbe()
- probe watch target
- target ! PoisonPill
- probe.expectTerminated(target)
Replying to Messages Received by Probes
The probes keep track of the communications channel for replies, if possible, so they can also reply:
- val probe = TestProbe()
- val future = probe.ref ? "hello"
- probe.expectMsg(0 millis, "hello") // TestActor runs on CallingThreadDispatcher
- probe.reply("world")
- assert(future.isCompleted && future.value == Some(Success("world")))
Forwarding Messages Received by Probes
Given a destination actor dest which in the nominal actor network would receive a message from actor source. If you arrange for the message to be sent to a TestProbe probe instead, you can make assertions concerning volume and timing of the message flow while still keeping the network functioning:
- class Source(target: ActorRef) extends Actor {
- def receive = {
- case "start" => target ! "work"
- }
- }
-
- class Destination extends Actor {
- def receive = {
- case x => // Do something..
- }
- }
- val probe = TestProbe()
- val source = system.actorOf(Props(classOf[Source], probe.ref))
- val dest = system.actorOf(Props[Destination])
- source ! "start"
- probe.expectMsg("work")
- probe.forward(dest)
The dest actor will receive the same message invocation as if no test probe had intervened.
Auto-Pilot
Receiving messages in a queue for later inspection is nice, but in order to keep a test running and verify traces later you can also install an AutoPilot in the participating test probes (actually in any TestKit) which is invoked before enqueueing to the inspection queue. This code can be used to forward messages, e.g. in a chain A --> Probe --> B, as long as a certain protocol is obeyed.
- val probe = TestProbe()
- probe.setAutoPilot(new TestActor.AutoPilot {
- def run(sender: ActorRef, msg: Any): TestActor.AutoPilot =
- msg match {
- case "stop" ⇒ TestActor.NoAutoPilot
- case x ⇒ testActor.tell(x, sender); TestActor.KeepRunning
- }
- })
The run method must return the auto-pilot for the next message, which may be KeepRunning to retain the current one or NoAutoPilot to switch it off.
Caution about Timing Assertions
The behavior of within blocks when using test probes might be perceived as counter-intuitive: you need to remember that the nicely scoped deadline as described above is local to each probe. Hence, probes do not react to each other's deadlines or to the deadline set in an enclosing TestKit instance:
- val probe = TestProbe()
- within(1 second) {
- probe.expectMsg("hello")
- }
Here, the expectMsg call will use the default timeout.
Testing parent-child relationships
The parent of an actor is always the actor that created it. At times this leads to a coupling between the two that may not be straightforward to test. Broadly, there are three approaches to improve testability of parent-child relationships:
- when creating a child, pass an explicit reference to its parent
- when creating a parent, tell the parent how to create its child
- create a fabricated parent when testing
For example, the structure of the code you want to test may follow this pattern:
- class Parent extends Actor {
- val child = context.actorOf(Props[Child], "child")
- var ponged = false
-
- def receive = {
- case "pingit" => child ! "ping"
- case "pong" => ponged = true
- }
- }
-
- class Child extends Actor {
- def receive = {
- case "ping" => context.parent ! "pong"
- }
- }
Using dependency-injection
The first option is to avoid use of the context.parent function and create a child with a custom parent by passing an explicit reference to its parent instead.
- class DependentChild(parent: ActorRef) extends Actor {
- def receive = {
- case "ping" => parent ! "pong"
- }
- }
Alternatively, you can tell the parent how to create its child. There are two ways to do this: by giving it a Props object or by giving it a function which takes care of creating the child actor:
- class DependentParent(childProps: Props) extends Actor {
- val child = context.actorOf(childProps, "child")
- var ponged = false
-
- def receive = {
- case "pingit" => child ! "ping"
- case "pong" => ponged = true
- }
- }
-
- class GenericDependentParent(childMaker: ActorRefFactory => ActorRef) extends Actor {
- val child = childMaker(context)
- var ponged = false
-
- def receive = {
- case "pingit" => child ! "ping"
- case "pong" => ponged = true
- }
- }
Creating the Props is straightforward and the function may look like this in your test code:
- val maker = (_: ActorRefFactory) => probe.ref
- val parent = system.actorOf(Props(classOf[GenericDependentParent], maker))
And like this in your application code:
- val maker = (f: ActorRefFactory) => f.actorOf(Props[Child])
- val parent = system.actorOf(Props(classOf[GenericDependentParent], maker))
Using a fabricated parent
If you prefer to avoid modifying the parent or child constructor you can create a fabricated parent in your test. This, however, does not enable you to test the parent actor in isolation.
- "A fabricated parent" should {
- "test its child responses" in {
- val proxy = TestProbe()
- val parent = system.actorOf(Props(new Actor {
- val child = context.actorOf(Props[Child], "child")
- def receive = {
- case x if sender == child => proxy.ref forward x
- case x => child forward x
- }
- }))
-
- proxy.send(parent, "ping")
- proxy.expectMsg("pong")
- }
- }
Which of these methods is the best depends on what is most important to test. The most generic option is to create the parent actor by passing it a function that is responsible for the Actor creation, but the fabricated parent is often sufficient.
CallingThreadDispatcher
The CallingThreadDispatcher serves good purposes in unit testing, as described above, but originally it was conceived in order to allow contiguous stack traces to be generated in case of an error. As this special dispatcher runs everything which would normally be queued directly on the current thread, the full history of a message's processing chain is recorded on the call stack, so long as all intervening actors run on this dispatcher.
How to use it
Just set the dispatcher as you normally would:
- import akka.testkit.CallingThreadDispatcher
- val ref = system.actorOf(Props[MyActor].withDispatcher(CallingThreadDispatcher.Id))
How it works
When receiving an invocation, the CallingThreadDispatcher checks whether the receiving actor is already active on the current thread. The simplest example for this situation is an actor which sends a message to itself. In this case, processing cannot continue immediately as that would violate the actor model, so the invocation is queued and will be processed when the active invocation on that actor finishes its processing; thus, it will be processed on the calling thread, but simply after the actor finishes its previous work. In the other case, the invocation is simply processed immediately on the current thread. Futures scheduled via this dispatcher are also executed immediately.
This scheme makes the CallingThreadDispatcher work like a general purpose dispatcher for any actors which never block on external events.
In the presence of multiple threads it may happen that two invocations of an actor running on this dispatcher happen on two different threads at the same time. In this case, both will be processed directly on their respective threads, where both compete for the actor's lock and the loser has to wait. Thus, the actor model is left intact, but the price is loss of concurrency due to limited scheduling. In a sense this is equivalent to traditional mutex style concurrency.
The other remaining difficulty is correct handling of suspend and resume: when an actor is suspended, subsequent invocations will be queued in thread-local queues (the same ones used for queuing in the normal case). The call toresume, however, is done by one specific thread, and all other threads in the system will probably not be executing this specific actor, which leads to the problem that the thread-local queues cannot be emptied by their native threads. Hence, the thread calling resume will collect all currently queued invocations from all threads into its own queue and process them.
Limitations
Warning
In case the CallingThreadDispatcher is used for top-level actors, but without going through TestActorRef, then there is a time window during which the actor is awaiting construction by the user guardian actor. Sending messages to the actor during this time period will result in them being enqueued and then executed on the guardian’s thread instead of the caller’s thread. To avoid this, use TestActorRef.
If an actor's behavior blocks on a something which would normally be affected by the calling actor after having sent the message, this will obviously dead-lock when using this dispatcher. This is a common scenario in actor tests based onCountDownLatch for synchronization:
- val latch = new CountDownLatch(1)
- actor ! startWorkAfter(latch) // actor will call latch.await() before proceeding
- doSomeSetupStuff()
- latch.countDown()
The example would hang indefinitely within the message processing initiated on the second line and never reach the fourth line, which would unblock it on a normal dispatcher.
Thus, keep in mind that the CallingThreadDispatcher is not a general-purpose replacement for the normal dispatchers. On the other hand it may be quite useful to run your actor network on it for testing, because if it runs without dead-locking chances are very high that it will not dead-lock in production.
Warning
The above sentence is unfortunately not a strong guarantee, because your code might directly or indirectly change its behavior when running on a different dispatcher. If you are looking for a tool to help you debug dead-locks, theCallingThreadDispatcher may help with certain error scenarios, but keep in mind that it has may give false negatives as well as false positives.
Thread Interruptions
If the CallingThreadDispatcher sees that the current thread has its isInterrupted() flag set when message processing returns, it will throw an InterruptedException after finishing all its processing (i.e. all messages which need processing as described above are processed before this happens). As tell cannot throw exceptions due to its contract, this exception will then be caught and logged, and the thread’s interrupted status will be set again.
If during message processing an InterruptedException is thrown then it will be caught inside the CallingThreadDispatcher’s message handling loop, the thread’s interrupted flag will be set and processing continues normally.
Note
The summary of these two paragraphs is that if the current thread is interrupted while doing work under the CallingThreadDispatcher, then that will result in the isInterrupted flag to be true when the message send returns and no InterruptedException will be thrown.
Benefits
To summarize, these are the features with the CallingThreadDispatcher has to offer:
- Deterministic execution of single-threaded tests while retaining nearly full actor semantics
- Full message processing history leading up to the point of failure in exception stack traces
- Exclusion of certain classes of dead-lock scenarios
Tracing Actor Invocations
The testing facilities described up to this point were aiming at formulating assertions about a system’s behavior. If a test fails, it is usually your job to find the cause, fix it and verify the test again. This process is supported by debuggers as well as logging, where the Akka toolkit offers the following options:
Logging of exceptions thrown within Actor instances
This is always on; in contrast to the other logging mechanisms, this logs at ERROR level.
Logging of message invocations on certain actors
This is enabled by a setting in the Configuration — namely akka.actor.debug.receive — which enables theloggable statement to be applied to an actor’s receive function:
- import akka.event.LoggingReceive
- def receive = LoggingReceive {
- case msg => // Do something ...
- }
- def otherState: Receive = LoggingReceive.withLabel("other") {
- case msg => // Do something else ...
- }
If the aforementioned setting is not given in the Configuration, this method will pass through the given Receivefunction unmodified, meaning that there is no runtime cost unless actually enabled.
The logging feature is coupled to this specific local mark-up because enabling it uniformly on all actors is not usually what you need, and it would lead to endless loops if it were applied to event bus logger listeners.
Logging of special messages
Actors handle certain special messages automatically, e.g. Kill, PoisonPill, etc. Tracing of these message invocations is enabled by the setting akka.actor.debug.autoreceive, which enables this on all actors.
Logging of the actor lifecycle
Actor creation, start, restart, monitor start, monitor stop and stop may be traced by enabling the settingakka.actor.debug.lifecycle; this, too, is enabled uniformly on all actors.
All these messages are logged at DEBUG level. To summarize, you can enable full logging of actor activities using this configuration fragment:
- akka {
- loglevel = "DEBUG"
- actor {
- debug {
- receive = on
- autoreceive = on
- lifecycle = on
- }
- }
- }
Different Testing Frameworks
Akka’s own test suite is written using ScalaTest, which also shines through in documentation examples. However, the TestKit and its facilities do not depend on that framework, you can essentially use whichever suits your development style best.
This section contains a collection of known gotchas with some other frameworks, which is by no means exhaustive and does not imply endorsement or special support.
When you need it to be a trait
If for some reason it is a problem to inherit from TestKit due to it being a concrete class instead of a trait, there’sTestKitBase:
- import akka.testkit.TestKitBase
-
- class MyTest extends TestKitBase {
- implicit lazy val system = ActorSystem()
-
- // put your test code here ...
-
- shutdown(system)
- }
The implicit lazy val system must be declared exactly like that (you can of course pass arguments to the actor system factory as needed) because trait TestKitBase needs the system during its construction.
Warning
Use of the trait is discouraged because of potential issues with binary backwards compatibility in the future, use at own risk.
Specs2
Some Specs2 users have contributed examples of how to work around some clashes which may arise:
- Mixing TestKit into
org.specs2.mutable.Specification results in a name clash involving the end method (which is a private variable in TestKit and an abstract method in Specification); if mixing in TestKit first, the code may compile but might then fail at runtime. The work-around—which is actually beneficial also for the third point—is to apply the TestKit together with org.specs2.specification.Scope. - The Specification traits provide a
Duration DSL which uses partly the same method names asscala.concurrent.duration.Duration, resulting in ambiguous implicits if scala.concurrent.duration._ is imported. There are two workarounds:- either use the Specification variant of Duration and supply an implicit conversion to the Akka Duration. This conversion is not supplied with the Akka distribution because that would mean that our JAR files would depend on Specs2, which is not justified by this little feature.
- or mix
org.specs2.time.NoTimeConversions into the Specification.
- Specifications are by default executed concurrently, which requires some care when writing the tests or alternatively the
sequential keyword.
Configuration
There are several configuration properties for the TestKit module, please refer to the reference configuration.
Actor DSL
The Actor DSL
Simple actors—for example one-off workers or even when trying things out in the REPL—can be created more concisely using the Act trait. The supporting infrastructure is bundled in the following import:
- import akka.actor.ActorDSL._
- import akka.actor.ActorSystem
-
- implicit val system = ActorSystem("demo")
This import is assumed for all code samples throughout this section. The implicit actor system serves asActorRefFactory for all examples below. To define a simple actor, the following is sufficient:
- val a = actor(new Act {
- become {
- case "hello" ⇒ sender() ! "hi"
- }
- })
Here, actor takes the role of either system.actorOf or context.actorOf, depending on which context it is called in: it takes an implicit ActorRefFactory, which within an actor is available in the form of theimplicit val context: ActorContext. Outside of an actor, you’ll have to either declare an implicit ActorSystem, or you can give the factory explicitly (see further below).
The two possible ways of issuing a context.become (replacing or adding the new behavior) are offered separately to enable a clutter-free notation of nested receives:
- val a = actor(new Act {
- become { // this will replace the initial (empty) behavior
- case "info" ⇒ sender() ! "A"
- case "switch" ⇒
- becomeStacked { // this will stack upon the "A" behavior
- case "info" ⇒ sender() ! "B"
- case "switch" ⇒ unbecome() // return to the "A" behavior
- }
- case "lobotomize" ⇒ unbecome() // OH NOES: Actor.emptyBehavior
- }
- })
Please note that calling unbecome more often than becomeStacked results in the original behavior being installed, which in case of the Act trait is the empty behavior (the outer become just replaces it during construction).
Life-cycle management
Life-cycle hooks are also exposed as DSL elements (see Start Hook and Stop Hook), where later invocations of the methods shown below will replace the contents of the respective hooks:
- val a = actor(new Act {
- whenStarting { testActor ! "started" }
- whenStopping { testActor ! "stopped" }
- })
The above is enough if the logical life-cycle of the actor matches the restart cycles (i.e. whenStopping is executed before a restart and whenStarting afterwards). If that is not desired, use the following two hooks (see Restart Hooks):
- val a = actor(new Act {
- become {
- case "die" ⇒ throw new Exception
- }
- whenFailing { case m @ (cause, msg) ⇒ testActor ! m }
- whenRestarted { cause ⇒ testActor ! cause }
- })
It is also possible to create nested actors, i.e. grand-children, like this:
- // here we pass in the ActorRefFactory explicitly as an example
- val a = actor(system, "fred")(new Act {
- val b = actor("barney")(new Act {
- whenStarting { context.parent ! ("hello from " + self.path) }
- })
- become {
- case x ⇒ testActor ! x
- }
- })
Note
In some cases it will be necessary to explicitly pass the ActorRefFactory to the actor method (you will notice when the compiler tells you about ambiguous implicits).
The grand-child will be supervised by the child; the supervisor strategy for this relationship can also be configured using a DSL element (supervision directives are part of the Act trait):
- superviseWith(OneForOneStrategy() {
- case e: Exception if e.getMessage == "hello" ⇒ Stop
- case _: Exception ⇒ Resume
- })
Actor with Stash
Last but not least there is a little bit of convenience magic built-in, which detects if the runtime class of the statically given actor subtype extends the RequiresMessageQueue trait via the Stash trait (this is a complicated way of saying that new Act with Stash would not work because its runtime erased type is just an anonymous subtype of Act). The purpose is to automatically use the appropriate deque-based mailbox type required by Stash. If you want to use this magic, simply extend ActWithStash:
- val a = actor(new ActWithStash {
- become {
- case 1 ⇒ stash()
- case 2 ⇒
- testActor ! 2; unstashAll(); becomeStacked {
- case 1 ⇒ testActor ! 1; unbecome()
- }
- }
- })
Типізовані актори
Note
This module will be deprecated as it will be superseded by the Akka Typed project which is currently being developed in open preview mode.
Akka Typed Actors is an implementation of the Active Objects pattern. Essentially turning method invocations into asynchronous dispatch instead of synchronous that has been the default way since Smalltalk came out.
Typed Actors consist of 2 "parts", a public interface and an implementation, and if you've done any work in "enterprise" Java, this will be very familiar to you. As with normal Actors you have an external API (the public interface instance) that will delegate method calls asynchronously to a private instance of the implementation.
The advantage of Typed Actors vs. Actors is that with TypedActors you have a static contract, and don't need to define your own messages, the downside is that it places some limitations on what you can do and what you can't, i.e. you cannot use become/unbecome.
Typed Actors are implemented using JDK Proxies which provide a pretty easy-worked API to intercept method calls.
Note
Just as with regular Akka Actors, Typed Actors process one call at a time.
When to use Typed Actors
Typed actors are nice for bridging between actor systems (the “inside”) and non-actor code (the “outside”), because they allow you to write normal OO-looking code on the outside. Think of them like doors: their practicality lies in interfacing between private sphere and the public, but you don’t want that many doors inside your house, do you? For a longer discussion see this blog post.
A bit more background: TypedActors can easily be abused as RPC, and that is an abstraction which is well-known to be leaky. Hence TypedActors are not what we think of first when we talk about making highly scalable concurrent software easier to write correctly. They have their niche, use them sparingly.
Creating Typed Actors
To create a Typed Actor you need to have one or more interfaces, and one implementation.
Our example interface:
- trait Squarer {
- def squareDontCare(i: Int): Unit //fire-forget
-
- def square(i: Int): Future[Int] //non-blocking send-request-reply
-
- def squareNowPlease(i: Int): Option[Int] //blocking send-request-reply
-
- def squareNow(i: Int): Int //blocking send-request-reply
-
- @throws(classOf[Exception]) //declare it or you will get an UndeclaredThrowableException
- def squareTry(i: Int): Int //blocking send-request-reply with possible exception
- }
Alright, now we've got some methods we can call, but we need to implement those in SquarerImpl.
- class SquarerImpl(val name: String) extends Squarer {
-
- def this() = this("default")
- def squareDontCare(i: Int): Unit = i * i //Nobody cares :(
-
- def square(i: Int): Future[Int] = Future.successful(i * i)
-
- def squareNowPlease(i: Int): Option[Int] = Some(i * i)
-
- def squareNow(i: Int): Int = i * i
-
- def squareTry(i: Int): Int = throw new Exception("Catch me!")
- }
Excellent, now we have an interface and an implementation of that interface, and we know how to create a Typed Actor from that, so let's look at calling these methods.
The most trivial way of creating a Typed Actor instance of our Squarer:
- val mySquarer: Squarer =
- TypedActor(system).typedActorOf(TypedProps[SquarerImpl]())
First type is the type of the proxy, the second type is the type of the implementation. If you need to call a specific constructor you do it like this:
- val otherSquarer: Squarer =
- TypedActor(system).typedActorOf(TypedProps(
- classOf[Squarer],
- new SquarerImpl("foo")), "name")
Since you supply a Props, you can specify which dispatcher to use, what the default timeout should be used and more.
Method dispatch semantics
Methods returning:
Unit will be dispatched with fire-and-forget semantics, exactly like ActorRef.tellscala.concurrent.Future[_] will use send-request-reply semantics, exactly like ActorRef.askscala.Option[_] will use send-request-reply semantics, but will block to wait for an answer, and returnscala.None if no answer was produced within the timeout, or scala.Some[_] containing the result otherwise. Any exception that was thrown during this call will be rethrown.- Any other type of value will use
send-request-reply semantics, but will block to wait for an answer, throwingjava.util.concurrent.TimeoutException if there was a timeout or rethrow any exception that was thrown during this call.
Messages and immutability
While Akka cannot enforce that the parameters to the methods of your Typed Actors are immutable, we stronglyrecommend that parameters passed are immutable.
One-way message send
- mySquarer.squareDontCare(10)
As simple as that! The method will be executed on another thread; asynchronously.
Request-reply message send
- val oSquare = mySquarer.squareNowPlease(10) //Option[Int]
This will block for as long as the timeout that was set in the Props of the Typed Actor, if needed. It will return None if a timeout occurs.
- val iSquare = mySquarer.squareNow(10) //Int
This will block for as long as the timeout that was set in the Props of the Typed Actor, if needed. It will throw ajava.util.concurrent.TimeoutException if a timeout occurs.
Request-reply-with-future message send
- val fSquare = mySquarer.square(10) //A Future[Int]
This call is asynchronous, and the Future returned can be used for asynchronous composition.
Stopping Typed Actors
Since Akka's Typed Actors are backed by Akka Actors they must be stopped when they aren't needed anymore.
- TypedActor(system).stop(mySquarer)
This asynchronously stops the Typed Actor associated with the specified proxy ASAP.
- TypedActor(system).poisonPill(otherSquarer)
This asynchronously stops the Typed Actor associated with the specified proxy after it's done with all calls that were made prior to this call.
Typed Actor Hierarchies
Since you can obtain a contextual Typed Actor Extension by passing in an ActorContext you can create child Typed Actors by invoking typedActorOf(..) on that:
- //Inside your Typed Actor
- val childSquarer: Squarer =
- TypedActor(TypedActor.context).typedActorOf(TypedProps[SquarerImpl]())
- //Use "childSquarer" as a Squarer
You can also create a child Typed Actor in regular Akka Actors by giving the ActorContext as an input parameter to TypedActor.get(…).
Supervisor Strategy
By having your Typed Actor implementation class implement TypedActor.Supervisor you can define the strategy to use for supervising child actors, as described in Supervision and Monitoring and Fault Tolerance.
Lifecycle callbacks
By having your Typed Actor implementation class implement any and all of the following:
TypedActor.PreStartTypedActor.PostStopTypedActor.PreRestartTypedActor.PostRestart
You can hook into the lifecycle of your Typed Actor.
Receive arbitrary messages
If your implementation class of your TypedActor extends akka.actor.TypedActor.Receiver, all messages that are not MethodCall instances will be passed into the onReceive-method.
This allows you to react to DeathWatch Terminated-messages and other types of messages, e.g. when interfacing with untyped actors.
Proxying
You can use the typedActorOf that takes a TypedProps and an ActorRef to proxy the given ActorRef as a TypedActor. This is usable if you want to communicate remotely with TypedActors on other machines, just pass the ActorRef totypedActorOf.
Note
The ActorRef needs to accept MethodCall messages.
Lookup & Remoting
Since TypedActors are backed by Akka Actors, you can use typedActorOf to proxy ActorRefs potentially residing on remote nodes.
- val typedActor: Foo with Bar =
- TypedActor(system).
- typedActorOf(
- TypedProps[FooBar],
- actorRefToRemoteActor)
- //Use "typedActor" as a FooBar
Supercharging
Here's an example on how you can use traits to mix in behavior in your Typed Actors.
- trait Foo {
- def doFoo(times: Int): Unit = println("doFoo(" + times + ")")
- }
-
- trait Bar {
- def doBar(str: String): Future[String] =
- Future.successful(str.toUpperCase)
- }
-
- class FooBar extends Foo with Bar
- val awesomeFooBar: Foo with Bar =
- TypedActor(system).typedActorOf(TypedProps[FooBar]())
-
- awesomeFooBar.doFoo(10)
- val f = awesomeFooBar.doBar("yes")
-
- TypedActor(system).poisonPill(awesomeFooBar)
Typed Router pattern
Sometimes you want to spread messages between multiple actors. The easiest way to achieve this in Akka is to use aRouter, which can implement a specific routing logic, such as smallest-mailbox or consistent-hashing etc.
Routers are not provided directly for typed actors, but it is really easy to leverage an untyped router and use a typed proxy in front of it. To showcase this let's create typed actors that assign themselves some random id, so we know that in fact, the router has sent the message to different actors:
- trait HasName {
- def name(): String
- }
-
- class Named extends HasName {
- import scala.util.Random
- private val id = Random.nextInt(1024)
-
- def name(): String = "name-" + id
- }
In order to round robin among a few instances of such actors, you can simply create a plain untyped router, and then facade it with a TypedActor like shown in the example below. This works because typed actors of course communicate using the same mechanisms as normal actors, and methods calls on them get transformed into message sends of MethodCall messages.
- def namedActor(): HasName = TypedActor(system).typedActorOf(TypedProps[Named]())
-
- // prepare routees
- val routees: List[HasName] = List.fill(5) { namedActor() }
- val routeePaths = routees map { r =>
- TypedActor(system).getActorRefFor(r).path.toStringWithoutAddress
- }
-
- // prepare untyped router
- val router: ActorRef = system.actorOf(RoundRobinGroup(routeePaths).props())
-
- // prepare typed proxy, forwarding MethodCall messages to `router`
- val typedRouter: HasName =
- TypedActor(system).typedActorOf(TypedProps[Named](), actorRef = router)
-
- println("actor was: " + typedRouter.name()) // name-184
- println("actor was: " + typedRouter.name()) // name-753
- println("actor was: " + typedRouter.name()) // name-320
- println("actor was: " + typedRouter.name()) // name-164
Futures
Introduction
In the Scala Standard Library, a Future is a data structure used to retrieve the result of some concurrent operation. This result can be accessed synchronously (blocking) or asynchronously (non-blocking).
Execution Contexts
In order to execute callbacks and operations, Futures need something called an ExecutionContext, which is very similar to a java.util.concurrent.Executor. if you have an ActorSystem in scope, it will use its default dispatcher as the ExecutionContext, or you can use the factory methods provided by the ExecutionContextcompanion object to wrap Executors and ExecutorServices, or even create your own.
- import scala.concurrent.{ ExecutionContext, Promise }
-
- implicit val ec = ExecutionContext.fromExecutorService(yourExecutorServiceGoesHere)
-
- // Do stuff with your brand new shiny ExecutionContext
- val f = Promise.successful("foo")
-
- // Then shut your ExecutionContext down at some
- // appropriate place in your program/application
- ec.shutdown()
Within Actors
Each actor is configured to be run on a MessageDispatcher, and that dispatcher doubles as an ExecutionContext. If the nature of the Future calls invoked by the actor matches or is compatible with the activities of that actor (e.g. all CPU bound and no latency requirements), then it may be easiest to reuse the dispatcher for running the Futures by importing context.dispatcher.
- class A extends Actor {
- import context.dispatcher
- val f = Future("hello")
- def receive = {
- // receive omitted ...
- }
- }
Use With Actors
There are generally two ways of getting a reply from an Actor: the first is by a sent message (actor ! msg), which only works if the original sender was an Actor) and the second is through a Future.
Using an Actor's ? method to send a message will return a Future:
- import scala.concurrent.Await
- import akka.pattern.ask
- import akka.util.Timeout
- import scala.concurrent.duration._
-
- implicit val timeout = Timeout(5 seconds)
- val future = actor ? msg // enabled by the “ask” import
- val result = Await.result(future, timeout.duration).asInstanceOf[String]
This will cause the current thread to block and wait for the Actor to 'complete' the Future with it's reply. Blocking is discouraged though as it will cause performance problems. The blocking operations are located in Await.result andAwait.ready to make it easy to spot where blocking occurs. Alternatives to blocking are discussed further within this documentation. Also note that the Future returned by an Actor is a Future[Any] since an Actor is dynamic. That is why the asInstanceOf is used in the above sample. When using non-blocking it is better to use the mapTomethod to safely try to cast a Future to an expected type:
- import scala.concurrent.Future
- import akka.pattern.ask
-
- val future: Future[String] = ask(actor, msg).mapTo[String]
The mapTo method will return a new Future that contains the result if the cast was successful, or aClassCastException if not. Handling Exceptions will be discussed further within this documentation.
To send the result of a Future to an Actor, you can use the pipe construct:
- import akka.pattern.pipe
- future pipeTo actor
Use Directly
A common use case within Akka is to have some computation performed concurrently without needing the extra utility of an Actor. If you find yourself creating a pool of Actors for the sole reason of performing a calculation in parallel, there is an easier (and faster) way:
- import scala.concurrent.Await
- import scala.concurrent.Future
- import scala.concurrent.duration._
-
- val future = Future {
- "Hello" + "World"
- }
- future foreach println
In the above code the block passed to Future will be executed by the default Dispatcher, with the return value of the block used to complete the Future (in this case, the result would be the string: "HelloWorld"). Unlike a Futurethat is returned from an Actor, this Future is properly typed, and we also avoid the overhead of managing anActor.
You can also create already completed Futures using the Future companion, which can be either successes:
- val future = Future.successful("Yay!")
Or failures:
- val otherFuture = Future.failed[String](new IllegalArgumentException("Bang!"))
It is also possible to create an empty Promise, to be filled later, and obtain the corresponding Future:
- val promise = Promise[String]()
- val theFuture = promise.future
- promise.success("hello")
Functional Futures
Scala's Future has several monadic methods that are very similar to the ones used by Scala's collections. These allow you to create 'pipelines' or 'streams' that the result will travel through.
Future is a Monad
The first method for working with Future functionally is map. This method takes a Function which performs some operation on the result of the Future, and returning a new result. The return value of the map method is anotherFuture that will contain the new result:
- val f1 = Future {
- "Hello" + "World"
- }
- val f2 = f1 map { x =>
- x.length
- }
- f2 foreach println
In this example we are joining two strings together within a Future. Instead of waiting for this to complete, we apply our function that calculates the length of the string using the map method. Now we have a second Future that will eventually contain an Int. When our original Future completes, it will also apply our function and complete the second Future with its result. When we finally get the result, it will contain the number 10. Our original Future still contains the string "HelloWorld" and is unaffected by the map.
The map method is fine if we are modifying a single Future, but if 2 or more Futures are involved map will not allow you to combine them together:
- val f1 = Future {
- "Hello" + "World"
- }
- val f2 = Future.successful(3)
- val f3 = f1 map { x =>
- f2 map { y =>
- x.length * y
- }
- }
- f3 foreach println
f3 is a Future[Future[Int]] instead of the desired Future[Int]. Instead, the flatMap method should be used:
- val f1 = Future {
- "Hello" + "World"
- }
- val f2 = Future.successful(3)
- val f3 = f1 flatMap { x =>
- f2 map { y =>
- x.length * y
- }
- }
- f3 foreach println
Composing futures using nested combinators it can sometimes become quite complicated and hard to read, in these cases using Scala's 'for comprehensions' usually yields more readable code. See next section for examples.
If you need to do conditional propagation, you can use filter:
- val future1 = Future.successful(4)
- val future2 = future1.filter(_ % 2 == 0)
-
- future2 foreach println
-
- val failedFilter = future1.filter(_ % 2 == 1).recover {
- // When filter fails, it will have a java.util.NoSuchElementException
- case m: NoSuchElementException => 0
- }
-
- failedFilter foreach println
For Comprehensions
Since Future has a map, filter and flatMap method it can be easily used in a 'for comprehension':
- val f = for {
- a <- Future(10 / 2) // 10 / 2 = 5
- b <- Future(a + 1) // 5 + 1 = 6
- c <- Future(a - 1) // 5 - 1 = 4
- if c > 3 // Future.filter
- } yield b * c // 6 * 4 = 24
-
- // Note that the execution of futures a, b, and c
- // are not done in parallel.
-
- f foreach println
Something to keep in mind when doing this is even though it looks like parts of the above example can run in parallel, each step of the for comprehension is run sequentially. This will happen on separate threads for each step but there isn't much benefit over running the calculations all within a single Future. The real benefit comes when the Futures are created first, and then combining them together.
Composing Futures
The example for comprehension above is an example of composing Futures. A common use case for this is combining the replies of several Actors into a single calculation without resorting to calling Await.result or Await.ready to block for each result. First an example of using Await.result:
- val f1 = ask(actor1, msg1)
- val f2 = ask(actor2, msg2)
-
- val a = Await.result(f1, 3 seconds).asInstanceOf[Int]
- val b = Await.result(f2, 3 seconds).asInstanceOf[Int]
-
- val f3 = ask(actor3, (a + b))
-
- val result = Await.result(f3, 3 seconds).asInstanceOf[Int]
Warning
Await.result and Await.ready are provided for exceptional situations where you must block, a good rule of thumb is to only use them if you know why you must block. For all other cases, use asynchronous composition as described below.
Here we wait for the results from the first 2 Actors before sending that result to the third Actor. We calledAwait.result 3 times, which caused our little program to block 3 times before getting our final result. Now compare that to this example:
- val f1 = ask(actor1, msg1)
- val f2 = ask(actor2, msg2)
-
- val f3 = for {
- a <- f1.mapTo[Int]
- b <- f2.mapTo[Int]
- c <- ask(actor3, (a + b)).mapTo[Int]
- } yield c
-
- f3 foreach println
Here we have 2 actors processing a single message each. Once the 2 results are available (note that we don't block to get these results!), they are being added together and sent to a third Actor, which replies with a string, which we assign to 'result'.
This is fine when dealing with a known amount of Actors, but can grow unwieldy if we have more than a handful. Thesequence and traverse helper methods can make it easier to handle more complex use cases. Both of these methods are ways of turning, for a subclass T of Traversable, T[Future[A]] into a Future[T[A]]. For example:
- // oddActor returns odd numbers sequentially from 1 as a List[Future[Int]]
- val listOfFutures = List.fill(100)(akka.pattern.ask(oddActor, GetNext).mapTo[Int])
-
- // now we have a Future[List[Int]]
- val futureList = Future.sequence(listOfFutures)
-
- // Find the sum of the odd numbers
- val oddSum = futureList.map(_.sum)
- oddSum foreach println
To better explain what happened in the example, Future.sequence is taking the List[Future[Int]] and turning it into a Future[List[Int]]. We can then use map to work with the List[Int] directly, and we find the sum of theList.
The traverse method is similar to sequence, but it takes a T[A] and a function A => Future[B] to return aFuture[T[B]], where T is again a subclass of Traversable. For example, to use traverse to sum the first 100 odd numbers:
- val futureList = Future.traverse((1 to 100).toList)(x => Future(x * 2 - 1))
- val oddSum = futureList.map(_.sum)
- oddSum foreach println
This is the same result as this example:
- val futureList = Future.sequence((1 to 100).toList.map(x => Future(x * 2 - 1)))
- val oddSum = futureList.map(_.sum)
- oddSum foreach println
But it may be faster to use traverse as it doesn't have to create an intermediate List[Future[Int]].
Then there's a method that's called fold that takes a start-value, a sequence of Futures and a function from the type of the start-value and the type of the futures and returns something with the same type as the start-value, and then applies the function to all elements in the sequence of futures, asynchronously, the execution will start when the last of the Futures is completed.
- // Create a sequence of Futures
- val futures = for (i <- 1 to 1000) yield Future(i * 2)
- val futureSum = Future.fold(futures)(0)(_ + _)
- futureSum foreach println
That's all it takes!
If the sequence passed to fold is empty, it will return the start-value, in the case above, that will be 0. In some cases you don't have a start-value and you're able to use the value of the first completing Future in the sequence as the start-value, you can use reduce, it works like this:
- // Create a sequence of Futures
- val futures = for (i <- 1 to 1000) yield Future(i * 2)
- val futureSum = Future.reduce(futures)(_ + _)
- futureSum foreach println
Same as with fold, the execution will be done asynchronously when the last of the Future is completed, you can also parallelize it by chunking your futures into sub-sequences and reduce them, and then reduce the reduced results again.
Callbacks
Sometimes you just want to listen to a Future being completed, and react to that not by creating a new Future, but by side-effecting. For this Scala supports onComplete, onSuccess and onFailure, of which the latter two are specializations of the first.
- future onSuccess {
- case "bar" => println("Got my bar alright!")
- case x: String => println("Got some random string: " + x)
- }
- future onFailure {
- case ise: IllegalStateException if ise.getMessage == "OHNOES" =>
- //OHNOES! We are in deep trouble, do something!
- case e: Exception =>
- //Do something else
- }
- future onComplete {
- case Success(result) => doSomethingOnSuccess(result)
- case Failure(failure) => doSomethingOnFailure(failure)
- }
Define Ordering
Since callbacks are executed in any order and potentially in parallel, it can be tricky at the times when you need sequential ordering of operations. But there's a solution and it's name is andThen. It creates a new Future with the specified callback, a Future that will have the same result as the Future it's called on, which allows for ordering like in the following sample:
- val result = Future { loadPage(url) } andThen {
- case Failure(exception) => log(exception)
- } andThen {
- case _ => watchSomeTV()
- }
- result foreach println
Auxiliary Methods
Future fallbackTo combines 2 Futures into a new Future, and will hold the successful value of the secondFuture if the first Future fails.
- val future4 = future1 fallbackTo future2 fallbackTo future3
- future4 foreach println
You can also combine two Futures into a new Future that will hold a tuple of the two Futures successful results, using the zip operation.
- val future3 = future1 zip future2 map { case (a, b) => a + " " + b }
- future3 foreach println
Exceptions
Since the result of a Future is created concurrently to the rest of the program, exceptions must be handled differently. It doesn't matter if an Actor or the dispatcher is completing the Future, if an Exception is caught the Futurewill contain it instead of a valid result. If a Future does contain an Exception, calling Await.result will cause it to be thrown again so it can be handled properly.
It is also possible to handle an Exception by returning a different result. This is done with the recover method. For example:
- val future = akka.pattern.ask(actor, msg1) recover {
- case e: ArithmeticException => 0
- }
- future foreach println
In this example, if the actor replied with a akka.actor.Status.Failure containing the ArithmeticException, ourFuture would have a result of 0. The recover method works very similarly to the standard try/catch blocks, so multiple Exceptions can be handled in this manner, and if an Exception is not handled this way it will behave as if we hadn't used the recover method.
You can also use the recoverWith method, which has the same relationship to recover as flatMap has to map, and is use like this:
- val future = akka.pattern.ask(actor, msg1) recoverWith {
- case e: ArithmeticException => Future.successful(0)
- case foo: IllegalArgumentException =>
- Future.failed[Int](new IllegalStateException("All br0ken!"))
- }
- future foreach println
After
akka.pattern.after makes it easy to complete a Future with a value or exception after a timeout.
- // TODO after is unfortunately shadowed by ScalaTest, fix as part of #3759
- // import akka.pattern.after
-
- val delayed = akka.pattern.after(200 millis, using = system.scheduler)(Future.failed(
- new IllegalStateException("OHNOES")))
- val future = Future { Thread.sleep(1000); "foo" }
- val result = Future firstCompletedOf Seq(future, delayed)
Агенти
Агенти в Akka надихались агентами в Clojure.
Агенти провадять асинхронні зміни власних розташуваннях. Агенти прив'язані до одного розташування зберігання на протязі всього життя, та їм дозволено змінювати тільки це розташування (на новий стан), що відбувається як result of an action. Update actions are functions that are asynchronously applied to the Agent's state and whose return value becomes the Agent's new state. The state of an Agent should be immutable.
While updates to Agents are asynchronous, the state of an Agent is always immediately available for reading by any thread (using get or apply) without any messages.
Agents are reactive. The update actions of all Agents get interleaved amongst threads in an ExecutionContext. At any point in time, at most one send action for each Agent is being executed. Actions dispatched to an agent from another thread will occur in the order they were sent, potentially interleaved with actions dispatched to the same agent from other threads.
Note
Agents are local to the node on which they are created. This implies that you should generally not include them in messages that may be passed to remote Actors or as constructor parameters for remote Actors; those remote Actors will not be able to read or update the Agent.
Creating Agents
Agents are created by invoking Agent(value) passing in the Agent's initial value and providing an implicitExecutionContext to be used for it, for these examples we're going to use the default global one, but YMMV:
- import scala.concurrent.ExecutionContext.Implicits.global
- import akka.agent.Agent
- val agent = Agent(5)
Reading an Agent's value
Agents can be dereferenced (you can get an Agent's value) by invoking the Agent with parentheses like this:
Or by using the get method:
Reading an Agent's current value does not involve any message passing and happens immediately. So while updates to an Agent are asynchronous, reading the state of an Agent is synchronous.
Updating Agents (send & alter)
You update an Agent by sending a function that transforms the current value or by sending just a new value. The Agent will apply the new value or function atomically and asynchronously. The update is done in a fire-forget manner and you are only guaranteed that it will be applied. There is no guarantee of when the update will be applied but dispatches to an Agent from a single thread will occur in order. You apply a value or a function by invoking the send function.
- // send a value, enqueues this change
- // of the value of the Agent
- agent send 7
-
- // send a function, enqueues this change
- // to the value of the Agent
- agent send (_ + 1)
- agent send (_ * 2)
You can also dispatch a function to update the internal state but on its own thread. This does not use the reactive thread pool and can be used for long-running or blocking operations. You do this with the sendOff method. Dispatches using either sendOff or send will still be executed in order.
- // the ExecutionContext you want to run the function on
- implicit val ec = someExecutionContext()
- // sendOff a function
- agent sendOff longRunningOrBlockingFunction
All send methods also have a corresponding alter method that returns a Future. See Futures for more information on Futures.
- // alter a value
- val f1: Future[Int] = agent alter 7
-
- // alter a function
- val f2: Future[Int] = agent alter (_ + 1)
- val f3: Future[Int] = agent alter (_ * 2)
- // the ExecutionContext you want to run the function on
- implicit val ec = someExecutionContext()
- // alterOff a function
- val f4: Future[Int] = agent alterOff longRunningOrBlockingFunction
Awaiting an Agent's value
You can also get a Future to the Agents value, that will be completed after the currently queued updates have completed:
- val future = agent.future
See Futures for more information on Futures.
Monadic usage
Agents are also monadic, allowing you to compose operations using for-comprehensions. In monadic usage, new Agents are created leaving the original Agents untouched. So the old values (Agents) are still available as-is. They are so-called 'persistent'.
Example of monadic usage:
- import scala.concurrent.ExecutionContext.Implicits.global
- val agent1 = Agent(3)
- val agent2 = Agent(5)
-
- // uses foreach
- for (value <- agent1)
- println(value)
-
- // uses map
- val agent3 = for (value <- agent1) yield value + 1
-
- // or using map directly
- val agent4 = agent1 map (_ + 1)
-
- // uses flatMap
- val agent5 = for {
- value1 <- agent1
- value2 <- agent2
- } yield value1 + value2
Configuration
There are several configuration properties for the agents module, please refer to the reference configuration.
Deprecated Transactional Agents
Agents participating in enclosing STM transaction is a deprecated feature in 2.3.
If an Agent is used within an enclosing transaction, then it will participate in that transaction. If you send to an Agent within a transaction then the dispatch to the Agent will be held until that transaction commits, and discarded if the transaction is aborted. Here's an example:
- import scala.concurrent.ExecutionContext.Implicits.global
- import akka.agent.Agent
- import scala.concurrent.duration._
- import scala.concurrent.stm._
-
- def transfer(from: Agent[Int], to: Agent[Int], amount: Int): Boolean = {
- atomic { txn =>
- if (from.get < amount) false
- else {
- from send (_ - amount)
- to send (_ + amount)
- true
- }
- }
- }
-
- val from = Agent(100)
- val to = Agent(20)
- val ok = transfer(from, to, 50)
-
- val fromValue = from.future // -> 50
- val toValue = to.future // -> 70
0
0
0
0